{"article":{"slug":"from-thin-air-to-bootable-images-the-tine-build-system","title":"From Thin Air to Bootable Images: The tine Build System","subtitle":null,"summary":"Martin Pitt and Daan De Meyer introduce tine: reproducible builds of RPM, Go, and Rust components into bootable images from minimal host requirements.","content_type":"blog_post","language":"en","canonical_url":"https://amutable.com/blog/tine-build-system","author":{"name":"Martin Pitt","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"amutable","url":"https://amutable.com/","listing_slug":null,"listing":null},"topics":[{"name":"Build Systems","slug":"build-systems","url":"https://listedarticles.com/topics/build-systems"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"},{"name":"Linux","slug":"linux","url":"https://listedarticles.com/topics/linux"},{"name":"Engineering","slug":"engineering","url":"https://listedarticles.com/topics/engineering"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":4850,"reading_minutes":21,"published_at":"2026-09-24T12:00:00.000Z","added_at":"2026-09-26T09:19:22.159Z","updated_at":"2026-09-26T09:19:22.159Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/from-thin-air-to-bootable-images-the-tine-build-system","markdown_url":"https://listedarticles.com/articles/from-thin-air-to-bootable-images-the-tine-build-system.md","example":false,"citation":"Martin Pitt, amutable. \"From Thin Air to Bootable Images: The tine Build System.\" 24 Sept 2026. https://amutable.com/blog/tine-build-system (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://amutable.com/blog/tine-build-system"},"body_markdown":"# From Thin Air to Bootable Images: The tine Build System\n\nBy Daan De Meyer and Martin Pitt\n\n## Table of contents\n\nThis post is part of a series covering some of the open source work we have been doing in recent months. Today we introduce and publish *tine*, our new Buck2-based build system.\n\n## Our Requirements\n\nBuilding an operating system with cryptographically verifiable integrity has to start with a build system with these very properties. At the same time, we are part of the greater open source community and want both to contribute and re-use as much existing work as possible. We also aim for efficient development with fast turnaround.\n\nThis roughly translates into the following requirements for our build system:\n\n- **Minimal host requirements.** It must be self-contained and must minimize external dependencies, so that it can be run in any environment.\n- **Full control over inputs.** It must allow pinning every piece of software that goes into a product. It must support a choice of upstream distributions (Fedora, CentOS, Arch, Debian, etc.) and reuse existing packages where possible, while still making it easy to react quickly to CVEs and to diverge (either temporarily or permanently) from upstream packaging decisions when necessary.\n- **Integrated package machinery.** It must provide tooling for importing, updating, and merging imported packages.\n- **Cheap world rebuilds.** It must be able to rebuild the world on demand, e.g. after a gcc bump.\n- **Hermetic, reproducible builds.** All component and image builds must run in a hermetic environment and produce bitwise reproducible output.\n- **Native image builds.** It must be able to build bootable operating system images and systemd sysext images natively and concurrently.\n- **Monorepo-based iteration.** It must support maintaining the operating system in a single top-level monorepo for fast end-to-end iteration. A change to an imported rpm or to a Go or Rust component must be immediately buildable and testable across the full set of images, without intermediate commits or pushes and without elaborate version or dependency declarations.\n- **First-class custom components.** It must natively and efficiently build Go and Rust projects from pinned external repositories, for example kubernetes or varlink-http-bridge.\n- **Scanner compatibility.** Built images must work with standard SBOM tooling and security scanners such as syft/grype or trivy.\n- **Caching.** Builds must be able to retrieve unchanged components from a local and/or global cache. Building everything from scratch can take hours, and a developer is usually only working on a single component.\n\n## Existing Tools\n\nBefore building our own build tool, we evaluated several options.\n\n### mkosi\n\nGiven our team includes the creator and maintainer of `mkosi`, it was a natural first candidate to evaluate. But we quickly came to the conclusion that it has some fundamental shortcomings. It’s great at building individual images based on upstream packages, but becomes restrictive when building several weakly related images or if you need more control over the artifacts that make up an image. Building multiple images is limited to images that are intended to be shipped as part of the “main” image.\n\nWe need to build many different kinds of artifacts in a uniform and robust way, not just images. Hence the build needs to be orchestrated by a generic and flexible tool. The main build file should be a language calling into *library functions* like “compile a cargo crate” or “build a UKI”.  mkosi is the opposite, it’s a *framework*: It knows how to build images, and only gives you free-form opaque hooks for the other kinds of builds. That leads to a bad experience when you want to build more than just images.\n\n### Open Build Service\n\nThe Open Build Service (OBS) is a powerful fully-integrated build system that is primarily used by SUSE and the openSUSE project to produce all of their artifacts, anything from packages to ISOs, and many other image formats. It has strong dependency tracking and supports a dizzying array of distributions.\n\nHowever, it’s also the antithesis of “minimal host requirements”. The server side of it is required, central, and non-trivial to self-host. It’s also not a generic build system, meaning any new artifact types would either have to be modelled as packages or require heavy patches to OBS. We concluded that this lack of flexibility combined with its overall architecture would make it difficult for OBS to meet our requirements.\n\n### BuildStream\n\nApache BuildStream describes the operating system image as a graph of YAML \"elements\", each with its own sources, dependencies and build commands. BuildStream builds each one in a bubblewrap sandbox and caches the result under a hash of everything that went into it, similar to Buck2. It is mature and used to build freedesktop-sdk, GNOME OS, and WebKitGTK.\n\nOur concerns with BuildStream are mostly around bootstrapping and extensibility. BuildStream is a Python application with compiled extensions and other dependencies. It relies on a separate set of helper programs, plus sandboxing tools from the host. Each of those can be pinned, but through different mechanisms, and even then the result still depends on the host's Python. In practice you run it from a pinned container image instead, but then you’re still dependent on an entire container runtime you don’t control.\n\nBuildStream’s YAML is a plain data format, and is extended with Python plugins. YAML has no functions, so over time you end up copy-pasting across the project. Ultimately we decided to go for a tool with a better bootstrapping and pinning story as well as a more flexible language.\n\n### Antlir\n\nAntlir is Meta's OS image builder, built on top of the Buck2 build system engine. Buck2 is Meta's open source build system with emphasis on correctness, flexibility, and caching as much as possible. Antlir implements various rules for building images with Buck2.\n\nAs Antlir is a high-level tool focused on Meta’s internal repository, it is naturally very opinionated and designed for Meta’s internal use cases. For example, it requires btrfs and is strongly focused on a single monorepo.\n\nWhile we decided against using Antlir itself, its underlying engine, Buck2, turned out to be a good fit, and we ended up choosing it as the foundation for our own build system.\n\n## Our Build System: tine\n\nIn essence, tine is a set of opinionated Buck2 rules to build rpm, Rust crate and Go module components, UKIs, and images; it can sign images either with a hardware key through PKCS#11 or a locally generated key. The intent is to combine the best ideas from Antlir and mkosi into a single tool.\n\ntine has only three requirements on its build host: git, python3 (just for its own bootstrapping, not for production builds) and user namespaces. From there, it bootstraps everything it needs from pinned declarations to get a reproducible and independent build environment. That can be a distribution as old or modern as you need.\n\nAn important tine concept is the “**box**”, which is a declared and pinned down environment to run a build task. Think containers or distrobox, but declared natively in Buck2’s language, and using Buck2’s caching and rebuild rules, so they build quickly and naturally, stay reproducible, and need no further dependencies to run. tine itself defines boxes for running rpmbuild, go, or cargo, or a bigger multi-purpose one called fedora.rawhide.box which contains e.g. systemd-ukify for building images and QEMU for running virtual machines. Your own project can define its own boxes.\n\n### tine’s Engine: Buck2\n\nTo better understand this post and the examples, here is a one-minute Buck2 primer for those familiar with Make or Meson:\n\n- **Build file:** A BUCK file is the directory's Makefile equivalent. It's written in a Python dialect called Starlark, and declares all*targets* that you can build.\n- **Cell:** A named build graph root; these roughly follow the boundaries of git repositories: // is your own top-level project (the OS you want to build), tine// is the tine checkout which your project pulls in.\n- **Target:** A named node in the build graph, i.e. one particular thing that you want to build. They are addressed with an absolute path of the form cell//directory/sub:name, which refers to a target name defined in cell’s directory/sub/BUCK file. Within a cell, you can also use relative paths, like`subdir:name` , or even just`:name` for a target in the current directory. A single target can publish several output variants (“subtargets”), e.g.`:my_cool_os[qcow2]` or`:my_cool_os[sbom]` .\n- **Rule:** The equivalent of a meson`*_target()` , or the structure of a Makefile rule: a Starlark expression which translates a target into a set of*actions* and their parameters. It does not run anything by itself. For example, a`bootable_disk(name = “myos”, param1 = …)` rule defines a`myos` target and invokes a`bootable_disk` rule which translates it into actions like “install rpms”, “run`systemd-repart”` and so on.\n- **Action:** One build command with its declared inputs and outputs, the equivalent of the commands in a Makefile rule. That abstraction allows running all of them consistently in a sandbox which only sees these inputs. When Starlark doesn’t suffice, these rules can be implemented with the full power of Python.\n\nMore information can be found on Buck2’s key concepts page.\n\nUnlike Make or Meson, Buck2 never decides what to rebuild from timestamps. An action is keyed by a hash of all of its inputs: the sources, the tool binaries, the build platform configuration, and the command line itself. That is what makes its incremental builds correct and trustworthy, and it also allows taking an action's result from a shared cache instead of re-running it.\n\n## Walkthrough: Building a Bootable Image\n\nLet’s walk through how to use tine in your own projects. We will build a very basic example from scratch: a bootable image based on Fedora Rawhide with a Go project, and boot it. In this example, we’ll use duf, a CLI tool that shows free/used disk space in a text terminal with nice ASCII art.\n\nLet’s follow tine's README and set up a fresh demo git repository which pulls in tine and initializes it.\n\n```\ngit init tine-demo\ncd tine-demo\ngit submodule add https://github.com/amutable-systems/tine tine\ntine/bin/tine init\ngit add .\ngit commit -m \"initialize\"\n```\nNow let’s add a BUCK file. We’ll walk through it in several blocks, but these all go in the same file. First we need to import some definitions. This is Starlark, so akin to Python’s import statements.\n\n```\nload(\"@tine//box:defs.bzl\", \"box\")\nload(\"@tine//git:defs.bzl\", \"git\")\nload(\"@tine//go:defs.bzl\", \"go\")\nload(\"@tine//image:defs.bzl\", \"image\")\n```\nNext we need to define a build environment for the Go compiler. tine already offers a Fedora rawhide catalog. So, let’s just use that (hence the tine// cell) and Fedora’s golang package. A real project would likely define and track their parent OS catalog by itself, instead of blindly following tine’s.\n\n```\nbox.new(\n    name = \"go.box\",\n    packages = [\"golang\"],\n    release = \"tine//catalog:fedora.rawhide.release\",\n)\n```\nDeclare the duf Go project git repository which we want to build. tine requires pinning every input exactly, so we specify a git commit ID. That git repository is then passed as input to the go.package() rule which binds the above go.box and the git checkout, both referenced as relative targets (see above), hence the colon separator.\n\n```\ngit.fetch(\n    name = \"duf.git\",\n    repo = \"https://github.com/muesli/duf\",\n    rev = \"4636deb4a7b707a9f04c602db033f9837e50b3f6\",\n)\ngo.package(\n    name = \"duf\",\n    box = \":go.box\",  # a target in the current directory\n    src = \":duf.git\", # another target\n)\n```\nWith that we can already build and execute the binary.\n\n```\ntine/bin/tine buck run :duf\n# [...]\n# BUILD SUCCEEDED - starting your binary\n# 5 local devices\n# [...]\n```\nAnd now for the last big piece: the bootable image. Just as with the Go box, we re-use the tine catalog’s package manager that gets packages from Fedora Rawhide. This is the minimum set to be able to boot in a virtual machine, plus bash. As an extra ops (operation) this installs the built hello binary from the above go rule.\n\n```\nimage.bootable_disk(\n    name = \"demo\",\n    package_manager = \"tine//catalog:fedora.rawhide.package-manager\",\n    definitions = image.DEFAULT_USR_VERITY_PARTITIONS,\n    version = \"0.0.0\",\n    package_sets = [\"bootable\"],\n    packages = [\"bash\"],\n    ops = [\n        image.copy(\":duf[duf]\", \"/usr/bin/duf\"),\n    ],\n)\n```\nWe can ask Buck2 for all available build targets in a cell.\n\n```\ntine/bin/tine buck targets //...\n# root//:demo\n# root//:demo.initrd\n# root//:duf\n# root//:duf.git\n# root//:example.git\n# root//:go.box\n# root//:go.box.exec\n```\nAfter all of that, we can now build the image. However, it’s far more interesting to actually see it live in QEMU. Let’s add a VM definition, with auto-login for convenience, that boots our shiny new demo image using QEMU and related tools from tine’s own Rawhide catalog.\n\n```\nimage.vm(\n    name = \"demo-vm\",\n    autologin = \"root\",\n    # re-using tine's rawhide box which has all of QEMU etc. installed\n    box = \"tine//catalog:fedora.rawhide.box\",\n    image = \":demo\",\n    credentials = {\n        \"firstboot.timezone\": \"UTC\",\n    }\n)\n```\nThis single command from a clean tree will then build the Go project, the image, and boot it.\n\n```\ntine/bin/tine buck run :demo-vm\n```\nYou should see the following.\n\n```\n[  OK  ] Reached target graphical.target - Graphical Interface.\nFedora Linux 46 (Rawhide Prerelease)\nKernel 7.3.0-0.rc3.260916g9b87fdc9af2f.34.fc46.x86_64 on an x86_64 (hvc0)\nfedora login: root (automatic login)\n-bash-5.3# duf --help\nUsage of duf:\n      --all   include pseudo, duplicate, inaccessible file systems\n[...]\n-bash-5.3# ...\n-bash-5.3# systemctl poweroff\n```\nLook at tine’s examples/ directory for BUCK and auxiliary files for various scenarios such as a SecureBoot/verity OS signed by either a generated or hardware key, how to build Go/Rust projects, the various kinds of rules and customizations which tine offers, or how to write integration tests.\n\n## SBOMs for Free\n\ntine gives you a lot more for free. For example, it integrates the Syft SBOM generator so you can ask it to build a CycloneDX standard SBOM. Let’s also specify an output path, so that you don’t have to fish it out of buck-out/:\n\n```\ntine/bin/tine buck build --out /tmp/demo.cdx.json :demo[sbom][cyclonedx]\n```\n## There’s Much More!\n\nThis blog post has only covered the basics. In order to get a fuller picture, please refer to the documentation. We think the following topics are the most helpful to get started.\n\n- The distro package machinery for importing, modifying, and syncing/merging packages from Fedora or Arch\n- Setting up a shared build cache, and the details of its threat model and design\n- Signing builds with hardware keys through PKCS#11\n- Including auditable cargo projects and Go projects into image builds\n- Using tine mount for rapid iteration on a built third-party git project component\n- tine comes with builtin tools for bumping dependencies and refreshing the catalog which you can re-use in your project to automate the regular housekeeping and get tested PRs for them, like this one\n- tine also supports Arch, BTW!\n\nThe next post in this series will be about the update and provisioning system we have built to distribute these and other images. We hope to see you then!\n\nThis post is part of a series covering some of the open source work we have been doing in recent months. Today we introduce and publish *tine*, our new Buck2-based build system.\n\n## Our Requirements\n\nBuilding an operating system with cryptographically verifiable integrity has to start with a build system with these very properties. At the same time, we are part of the greater open source community and want both to contribute and re-use as much existing work as possible. We also aim for efficient development with fast turnaround.\n\nThis roughly translates into the following requirements for our build system:\n\n- **Minimal host requirements.** It must be self-contained and must minimize external dependencies, so that it can be run in any environment.\n- **Full control over inputs.** It must allow pinning every piece of software that goes into a product. It must support a choice of upstream distributions (Fedora, CentOS, Arch, Debian, etc.) and reuse existing packages where possible, while still making it easy to react quickly to CVEs and to diverge (either temporarily or permanently) from upstream packaging decisions when necessary.\n- **Integrated package machinery.** It must provide tooling for importing, updating, and merging imported packages.\n- **Cheap world rebuilds.** It must be able to rebuild the world on demand, e.g. after a gcc bump.\n- **Hermetic, reproducible builds.** All component and image builds must run in a hermetic environment and produce bitwise reproducible output.\n- **Native image builds.** It must be able to build bootable operating system images and systemd sysext images natively and concurrently.\n- **Monorepo-based iteration.** It must support maintaining the operating system in a single top-level monorepo for fast end-to-end iteration. A change to an imported rpm or to a Go or Rust component must be immediately buildable and testable across the full set of images, without intermediate commits or pushes and without elaborate version or dependency declarations.\n- **First-class custom components.** It must natively and efficiently build Go and Rust projects from pinned external repositories, for example kubernetes or varlink-http-bridge.\n- **Scanner compatibility.** Built images must work with standard SBOM tooling and security scanners such as syft/grype or trivy.\n- **Caching.** Builds must be able to retrieve unchanged components from a local and/or global cache. Building everything from scratch can take hours, and a developer is usually only working on a single component.\n\n## Existing Tools\n\nBefore building our own build tool, we evaluated several options.\n\n### mkosi\n\nGiven our team includes the creator and maintainer of `mkosi`, it was a natural first candidate to evaluate. But we quickly came to the conclusion that it has some fundamental shortcomings. It’s great at building individual images based on upstream packages, but becomes restrictive when building several weakly related images or if you need more control over the artifacts that make up an image. Building multiple images is limited to images that are intended to be shipped as part of the “main” image.\n\nWe need to build many different kinds of artifacts in a uniform and robust way, not just images. Hence the build needs to be orchestrated by a generic and flexible tool. The main build file should be a language calling into *library functions* like “compile a cargo crate” or “build a UKI”.  mkosi is the opposite, it’s a *framework*: It knows how to build images, and only gives you free-form opaque hooks for the other kinds of builds. That leads to a bad experience when you want to build more than just images.\n\n### Open Build Service\n\nThe Open Build Service (OBS) is a powerful fully-integrated build system that is primarily used by SUSE and the openSUSE project to produce all of their artifacts, anything from packages to ISOs, and many other image formats. It has strong dependency tracking and supports a dizzying array of distributions.\n\nHowever, it’s also the antithesis of “minimal host requirements”. The server side of it is required, central, and non-trivial to self-host. It’s also not a generic build system, meaning any new artifact types would either have to be modelled as packages or require heavy patches to OBS. We concluded that this lack of flexibility combined with its overall architecture would make it difficult for OBS to meet our requirements.\n\n### BuildStream\n\nApache BuildStream describes the operating system image as a graph of YAML \"elements\", each with its own sources, dependencies and build commands. BuildStream builds each one in a bubblewrap sandbox and caches the result under a hash of everything that went into it, similar to Buck2. It is mature and used to build freedesktop-sdk, GNOME OS, and WebKitGTK.\n\nOur concerns with BuildStream are mostly around bootstrapping and extensibility. BuildStream is a Python application with compiled extensions and other dependencies. It relies on a separate set of helper programs, plus sandboxing tools from the host. Each of those can be pinned, but through different mechanisms, and even then the result still depends on the host's Python. In practice you run it from a pinned container image instead, but then you’re still dependent on an entire container runtime you don’t control.\n\nBuildStream’s YAML is a plain data format, and is extended with Python plugins. YAML has no functions, so over time you end up copy-pasting across the project. Ultimately we decided to go for a tool with a better bootstrapping and pinning story as well as a more flexible language.\n\n### Antlir\n\nAntlir is Meta's OS image builder, built on top of the Buck2 build system engine. Buck2 is Meta's open source build system with emphasis on correctness, flexibility, and caching as much as possible. Antlir implements various rules for building images with Buck2.\n\nAs Antlir is a high-level tool focused on Meta’s internal repository, it is naturally very opinionated and designed for Meta’s internal use cases. For example, it requires btrfs and is strongly focused on a single monorepo.\n\nWhile we decided against using Antlir itself, its underlying engine, Buck2, turned out to be a good fit, and we ended up choosing it as the foundation for our own build system.\n\n## Our Build System: tine\n\nIn essence, tine is a set of opinionated Buck2 rules to build rpm, Rust crate and Go module components, UKIs, and images; it can sign images either with a hardware key through PKCS#11 or a locally generated key. The intent is to combine the best ideas from Antlir and mkosi into a single tool.\n\ntine has only three requirements on its build host: git, python3 (just for its own bootstrapping, not for production builds) and user namespaces. From there, it bootstraps everything it needs from pinned declarations to get a reproducible and independent build environment. That can be a distribution as old or modern as you need.\n\nAn important tine concept is the “**box**”, which is a declared and pinned down environment to run a build task. Think containers or distrobox, but declared natively in Buck2’s language, and using Buck2’s caching and rebuild rules, so they build quickly and naturally, stay reproducible, and need no further dependencies to run. tine itself defines boxes for running rpmbuild, go, or cargo, or a bigger multi-purpose one called fedora.rawhide.box which contains e.g. systemd-ukify for building images and QEMU for running virtual machines. Your own project can define its own boxes.\n\n### tine’s Engine: Buck2\n\nTo better understand this post and the examples, here is a one-minute Buck2 primer for those familiar with Make or Meson:\n\n- **Build file:** A BUCK file is the directory's Makefile equivalent. It's written in a Python dialect called Starlark, and declares all*targets* that you can build.\n- **Cell:** A named build graph root; these roughly follow the boundaries of git repositories: // is your own top-level project (the OS you want to build), tine// is the tine checkout which your project pulls in.\n- **Target:** A named node in the build graph, i.e. one particular thing that you want to build. They are addressed with an absolute path of the form cell//directory/sub:name, which refers to a target name defined in cell’s directory/sub/BUCK file. Within a cell, you can also use relative paths, like`subdir:name` , or even just`:name` for a target in the current directory. A single target can publish several output variants (“subtargets”), e.g.`:my_cool_os[qcow2]` or`:my_cool_os[sbom]` .\n- **Rule:** The equivalent of a meson`*_target()` , or the structure of a Makefile rule: a Starlark expression which translates a target into a set of*actions* and their parameters. It does not run anything by itself. For example, a`bootable_disk(name = “myos”, param1 = …)` rule defines a`myos` target and invokes a`bootable_disk` rule which translates it into actions like “install rpms”, “run`systemd-repart”` and so on.\n- **Action:** One build command with its declared inputs and outputs, the equivalent of the commands in a Makefile rule. That abstraction allows running all of them consistently in a sandbox which only sees these inputs. When Starlark doesn’t suffice, these rules can be implemented with the full power of Python.\n\nMore information can be found on Buck2’s key concepts page.\n\nUnlike Make or Meson, Buck2 never decides what to rebuild from timestamps. An action is keyed by a hash of all of its inputs: the sources, the tool binaries, the build platform configuration, and the command line itself. That is what makes its incremental builds correct and trustworthy, and it also allows taking an action's result from a shared cache instead of re-running it.\n\n## Walkthrough: Building a Bootable Image\n\nLet’s walk through how to use tine in your own projects. We will build a very basic example from scratch: a bootable image based on Fedora Rawhide with a Go project, and boot it. In this example, we’ll use duf, a CLI tool that shows free/used disk space in a text terminal with nice ASCII art.\n\nLet’s follow tine's README and set up a fresh demo git repository which pulls in tine and initializes it.\n\n```\ngit init tine-demo\ncd tine-demo\ngit submodule add https://github.com/amutable-systems/tine tine\ntine/bin/tine init\ngit add .\ngit commit -m \"initialize\"\n```\nNow let’s add a BUCK file. We’ll walk through it in several blocks, but these all go in the same file. First we need to import some definitions. This is Starlark, so akin to Python’s import statements.\n\n```\nload(\"@tine//box:defs.bzl\", \"box\")\nload(\"@tine//git:defs.bzl\", \"git\")\nload(\"@tine//go:defs.bzl\", \"go\")\nload(\"@tine//image:defs.bzl\", \"image\")\n```\nNext we need to define a build environment for the Go compiler. tine already offers a Fedora rawhide catalog. So, let’s just use that (hence the tine// cell) and Fedora’s golang package. A real project would likely define and track their parent OS catalog by itself, instead of blindly following tine’s.\n\n```\nbox.new(\n    name = \"go.box\",\n    packages = [\"golang\"],\n    release = \"tine//catalog:fedora.rawhide.release\",\n)\n```\nDeclare the duf Go project git repository which we want to build. tine requires pinning every input exactly, so we specify a git commit ID. That git repository is then passed as input to the go.package() rule which binds the above go.box and the git checkout, both referenced as relative targets (see above), hence the colon separator.\n\n```\ngit.fetch(\n    name = \"duf.git\",\n    repo = \"https://github.com/muesli/duf\",\n    rev = \"4636deb4a7b707a9f04c602db033f9837e50b3f6\",\n)\ngo.package(\n    name = \"duf\",\n    box = \":go.box\",  # a target in the current directory\n    src = \":duf.git\", # another target\n)\n```\nWith that we can already build and execute the binary.\n\n```\ntine/bin/tine buck run :duf\n# [...]\n# BUILD SUCCEEDED - starting your binary\n# 5 local devices\n# [...]\n```\nAnd now for the last big piece: the bootable image. Just as with the Go box, we re-use the tine catalog’s package manager that gets packages from Fedora Rawhide. This is the minimum set to be able to boot in a virtual machine, plus bash. As an extra ops (operation) this installs the built hello binary from the above go rule.\n\n```\nimage.bootable_disk(\n    name = \"demo\",\n    package_manager = \"tine//catalog:fedora.rawhide.package-manager\",\n    definitions = image.DEFAULT_USR_VERITY_PARTITIONS,\n    version = \"0.0.0\",\n    package_sets = [\"bootable\"],\n    packages = [\"bash\"],\n    ops = [\n        image.copy(\":duf[duf]\", \"/usr/bin/duf\"),\n    ],\n)\n```\nWe can ask Buck2 for all available build targets in a cell.\n\n```\ntine/bin/tine buck targets //...\n# root//:demo\n# root//:demo.initrd\n# root//:duf\n# root//:duf.git\n# root//:example.git\n# root//:go.box\n# root//:go.box.exec\n```\nAfter all of that, we can now build the image. However, it’s far more interesting to actually see it live in QEMU. Let’s add a VM definition, with auto-login for convenience, that boots our shiny new demo image using QEMU and related tools from tine’s own Rawhide catalog.\n\n```\nimage.vm(\n    name = \"demo-vm\",\n    autologin = \"root\",\n    # re-using tine's rawhide box which has all of QEMU etc. installed\n    box = \"tine//catalog:fedora.rawhide.box\",\n    image = \":demo\",\n    credentials = {\n        \"firstboot.timezone\": \"UTC\",\n    }\n)\n```\nThis single command from a clean tree will then build the Go project, the image, and boot it.\n\n```\ntine/bin/tine buck run :demo-vm\n```\nYou should see the following.\n\n```\n[  OK  ] Reached target graphical.target - Graphical Interface.\nFedora Linux 46 (Rawhide Prerelease)\nKernel 7.3.0-0.rc3.260916g9b87fdc9af2f.34.fc46.x86_64 on an x86_64 (hvc0)\nfedora login: root (automatic login)\n-bash-5.3# duf --help\nUsage of duf:\n      --all   include pseudo, duplicate, inaccessible file systems\n[...]\n-bash-5.3# ...\n-bash-5.3# systemctl poweroff\n```\nLook at tine’s examples/ directory for BUCK and auxiliary files for various scenarios such as a SecureBoot/verity OS signed by either a generated or hardware key, how to build Go/Rust projects, the various kinds of rules and customizations which tine offers, or how to write integration tests.\n\n## SBOMs for Free\n\ntine gives you a lot more for free. For example, it integrates the Syft SBOM generator so you can ask it to build a CycloneDX standard SBOM. Let’s also specify an output path, so that you don’t have to fish it out of buck-out/:\n\n```\ntine/bin/tine buck build --out /tmp/demo.cdx.json :demo[sbom][cyclonedx]\n```\n## There’s Much More!\n\nThis blog post has only covered the basics. In order to get a fuller picture, please refer to the documentation. We think the following topics are the most helpful to get started.\n\n- The distro package machinery for importing, modifying, and syncing/merging packages from Fedora or Arch\n- Setting up a shared build cache, and the details of its threat model and design\n- Signing builds with hardware keys through PKCS#11\n- Including auditable cargo projects and Go projects into image builds\n- Using tine mount for rapid iteration on a built third-party git project component\n- tine comes with builtin tools for bumping dependencies and refreshing the catalog which you can re-use in your project to automate the regular housekeeping and get tested PRs for them, like this one\n- tine also supports Arch, BTW!\n\nThe next post in this series will be about the update and provisioning system we have built to distribute these and other images. We hope to see you then!","body_html":"<h1 id=\"from-thin-air-to-bootable-images-the-tine-build-system\">From Thin Air to Bootable Images: The tine Build System</h1>\n<p>By Daan De Meyer and Martin Pitt</p>\n<h2 id=\"table-of-contents\">Table of contents</h2>\n<p>This post is part of a series covering some of the open source work we have been doing in recent months. Today we introduce and publish <em>tine</em>, our new Buck2-based build system.</p>\n<h2 id=\"our-requirements\">Our Requirements</h2>\n<p>Building an operating system with cryptographically verifiable integrity has to start with a build system with these very properties. At the same time, we are part of the greater open source community and want both to contribute and re-use as much existing work as possible. We also aim for efficient development with fast turnaround.</p>\n<p>This roughly translates into the following requirements for our build system:</p>\n<ul><li><strong>Minimal host requirements.</strong> It must be self-contained and must minimize external dependencies, so that it can be run in any environment.</li><li><strong>Full control over inputs.</strong> It must allow pinning every piece of software that goes into a product. It must support a choice of upstream distributions (Fedora, CentOS, Arch, Debian, etc.) and reuse existing packages where possible, while still making it easy to react quickly to CVEs and to diverge (either temporarily or permanently) from upstream packaging decisions when necessary.</li><li><strong>Integrated package machinery.</strong> It must provide tooling for importing, updating, and merging imported packages.</li><li><strong>Cheap world rebuilds.</strong> It must be able to rebuild the world on demand, e.g. after a gcc bump.</li><li><strong>Hermetic, reproducible builds.</strong> All component and image builds must run in a hermetic environment and produce bitwise reproducible output.</li><li><strong>Native image builds.</strong> It must be able to build bootable operating system images and systemd sysext images natively and concurrently.</li><li><strong>Monorepo-based iteration.</strong> It must support maintaining the operating system in a single top-level monorepo for fast end-to-end iteration. A change to an imported rpm or to a Go or Rust component must be immediately buildable and testable across the full set of images, without intermediate commits or pushes and without elaborate version or dependency declarations.</li><li><strong>First-class custom components.</strong> It must natively and efficiently build Go and Rust projects from pinned external repositories, for example kubernetes or varlink-http-bridge.</li><li><strong>Scanner compatibility.</strong> Built images must work with standard SBOM tooling and security scanners such as syft/grype or trivy.</li><li><strong>Caching.</strong> Builds must be able to retrieve unchanged components from a local and/or global cache. Building everything from scratch can take hours, and a developer is usually only working on a single component.</li></ul>\n<h2 id=\"existing-tools\">Existing Tools</h2>\n<p>Before building our own build tool, we evaluated several options.</p>\n<h3 id=\"mkosi\">mkosi</h3>\n<p>Given our team includes the creator and maintainer of <code>mkosi</code>, it was a natural first candidate to evaluate. But we quickly came to the conclusion that it has some fundamental shortcomings. It’s great at building individual images based on upstream packages, but becomes restrictive when building several weakly related images or if you need more control over the artifacts that make up an image. Building multiple images is limited to images that are intended to be shipped as part of the “main” image.</p>\n<p>We need to build many different kinds of artifacts in a uniform and robust way, not just images. Hence the build needs to be orchestrated by a generic and flexible tool. The main build file should be a language calling into <em>library functions</em> like “compile a cargo crate” or “build a UKI”.  mkosi is the opposite, it’s a <em>framework</em>: It knows how to build images, and only gives you free-form opaque hooks for the other kinds of builds. That leads to a bad experience when you want to build more than just images.</p>\n<h3 id=\"open-build-service\">Open Build Service</h3>\n<p>The Open Build Service (OBS) is a powerful fully-integrated build system that is primarily used by SUSE and the openSUSE project to produce all of their artifacts, anything from packages to ISOs, and many other image formats. It has strong dependency tracking and supports a dizzying array of distributions.</p>\n<p>However, it’s also the antithesis of “minimal host requirements”. The server side of it is required, central, and non-trivial to self-host. It’s also not a generic build system, meaning any new artifact types would either have to be modelled as packages or require heavy patches to OBS. We concluded that this lack of flexibility combined with its overall architecture would make it difficult for OBS to meet our requirements.</p>\n<h3 id=\"buildstream\">BuildStream</h3>\n<p>Apache BuildStream describes the operating system image as a graph of YAML &quot;elements&quot;, each with its own sources, dependencies and build commands. BuildStream builds each one in a bubblewrap sandbox and caches the result under a hash of everything that went into it, similar to Buck2. It is mature and used to build freedesktop-sdk, GNOME OS, and WebKitGTK.</p>\n<p>Our concerns with BuildStream are mostly around bootstrapping and extensibility. BuildStream is a Python application with compiled extensions and other dependencies. It relies on a separate set of helper programs, plus sandboxing tools from the host. Each of those can be pinned, but through different mechanisms, and even then the result still depends on the host&#39;s Python. In practice you run it from a pinned container image instead, but then you’re still dependent on an entire container runtime you don’t control.</p>\n<p>BuildStream’s YAML is a plain data format, and is extended with Python plugins. YAML has no functions, so over time you end up copy-pasting across the project. Ultimately we decided to go for a tool with a better bootstrapping and pinning story as well as a more flexible language.</p>\n<h3 id=\"antlir\">Antlir</h3>\n<p>Antlir is Meta&#39;s OS image builder, built on top of the Buck2 build system engine. Buck2 is Meta&#39;s open source build system with emphasis on correctness, flexibility, and caching as much as possible. Antlir implements various rules for building images with Buck2.</p>\n<p>As Antlir is a high-level tool focused on Meta’s internal repository, it is naturally very opinionated and designed for Meta’s internal use cases. For example, it requires btrfs and is strongly focused on a single monorepo.</p>\n<p>While we decided against using Antlir itself, its underlying engine, Buck2, turned out to be a good fit, and we ended up choosing it as the foundation for our own build system.</p>\n<h2 id=\"our-build-system-tine\">Our Build System: tine</h2>\n<p>In essence, tine is a set of opinionated Buck2 rules to build rpm, Rust crate and Go module components, UKIs, and images; it can sign images either with a hardware key through PKCS#11 or a locally generated key. The intent is to combine the best ideas from Antlir and mkosi into a single tool.</p>\n<p>tine has only three requirements on its build host: git, python3 (just for its own bootstrapping, not for production builds) and user namespaces. From there, it bootstraps everything it needs from pinned declarations to get a reproducible and independent build environment. That can be a distribution as old or modern as you need.</p>\n<p>An important tine concept is the “<strong>box</strong>”, which is a declared and pinned down environment to run a build task. Think containers or distrobox, but declared natively in Buck2’s language, and using Buck2’s caching and rebuild rules, so they build quickly and naturally, stay reproducible, and need no further dependencies to run. tine itself defines boxes for running rpmbuild, go, or cargo, or a bigger multi-purpose one called fedora.rawhide.box which contains e.g. systemd-ukify for building images and QEMU for running virtual machines. Your own project can define its own boxes.</p>\n<h3 id=\"tine-s-engine-buck2\">tine’s Engine: Buck2</h3>\n<p>To better understand this post and the examples, here is a one-minute Buck2 primer for those familiar with Make or Meson:</p>\n<ul><li><strong>Build file:</strong> A BUCK file is the directory&#39;s Makefile equivalent. It&#39;s written in a Python dialect called Starlark, and declares all<em>targets</em> that you can build.</li><li><strong>Cell:</strong> A named build graph root; these roughly follow the boundaries of git repositories: // is your own top-level project (the OS you want to build), tine// is the tine checkout which your project pulls in.</li><li><strong>Target:</strong> A named node in the build graph, i.e. one particular thing that you want to build. They are addressed with an absolute path of the form cell//directory/sub:name, which refers to a target name defined in cell’s directory/sub/BUCK file. Within a cell, you can also use relative paths, like<code>subdir:name</code> , or even just<code>:name</code> for a target in the current directory. A single target can publish several output variants (“subtargets”), e.g.<code>:my_cool_os[qcow2]</code> or<code>:my_cool_os[sbom]</code> .</li><li><strong>Rule:</strong> The equivalent of a meson<code>*_target()</code> , or the structure of a Makefile rule: a Starlark expression which translates a target into a set of<em>actions</em> and their parameters. It does not run anything by itself. For example, a<code>bootable_disk(name = “myos”, param1 = …)</code> rule defines a<code>myos</code> target and invokes a<code>bootable_disk</code> rule which translates it into actions like “install rpms”, “run<code>systemd-repart”</code> and so on.</li><li><strong>Action:</strong> One build command with its declared inputs and outputs, the equivalent of the commands in a Makefile rule. That abstraction allows running all of them consistently in a sandbox which only sees these inputs. When Starlark doesn’t suffice, these rules can be implemented with the full power of Python.</li></ul>\n<p>More information can be found on Buck2’s key concepts page.</p>\n<p>Unlike Make or Meson, Buck2 never decides what to rebuild from timestamps. An action is keyed by a hash of all of its inputs: the sources, the tool binaries, the build platform configuration, and the command line itself. That is what makes its incremental builds correct and trustworthy, and it also allows taking an action&#39;s result from a shared cache instead of re-running it.</p>\n<h2 id=\"walkthrough-building-a-bootable-image\">Walkthrough: Building a Bootable Image</h2>\n<p>Let’s walk through how to use tine in your own projects. We will build a very basic example from scratch: a bootable image based on Fedora Rawhide with a Go project, and boot it. In this example, we’ll use duf, a CLI tool that shows free/used disk space in a text terminal with nice ASCII art.</p>\n<p>Let’s follow tine&#39;s README and set up a fresh demo git repository which pulls in tine and initializes it.</p>\n<pre><code>git init tine-demo\ncd tine-demo\ngit submodule add https://github.com/amutable-systems/tine tine\ntine/bin/tine init\ngit add .\ngit commit -m &quot;initialize&quot;</code></pre>\n<p>Now let’s add a BUCK file. We’ll walk through it in several blocks, but these all go in the same file. First we need to import some definitions. This is Starlark, so akin to Python’s import statements.</p>\n<pre><code>load(&quot;@tine//box:defs.bzl&quot;, &quot;box&quot;)\nload(&quot;@tine//git:defs.bzl&quot;, &quot;git&quot;)\nload(&quot;@tine//go:defs.bzl&quot;, &quot;go&quot;)\nload(&quot;@tine//image:defs.bzl&quot;, &quot;image&quot;)</code></pre>\n<p>Next we need to define a build environment for the Go compiler. tine already offers a Fedora rawhide catalog. So, let’s just use that (hence the tine// cell) and Fedora’s golang package. A real project would likely define and track their parent OS catalog by itself, instead of blindly following tine’s.</p>\n<pre><code>box.new(\n    name = &quot;go.box&quot;,\n    packages = [&quot;golang&quot;],\n    release = &quot;tine//catalog:fedora.rawhide.release&quot;,\n)</code></pre>\n<p>Declare the duf Go project git repository which we want to build. tine requires pinning every input exactly, so we specify a git commit ID. That git repository is then passed as input to the go.package() rule which binds the above go.box and the git checkout, both referenced as relative targets (see above), hence the colon separator.</p>\n<pre><code>git.fetch(\n    name = &quot;duf.git&quot;,\n    repo = &quot;https://github.com/muesli/duf&quot;,\n    rev = &quot;4636deb4a7b707a9f04c602db033f9837e50b3f6&quot;,\n)\ngo.package(\n    name = &quot;duf&quot;,\n    box = &quot;:go.box&quot;,  # a target in the current directory\n    src = &quot;:duf.git&quot;, # another target\n)</code></pre>\n<p>With that we can already build and execute the binary.</p>\n<pre><code>tine/bin/tine buck run :duf\n# [...]\n# BUILD SUCCEEDED - starting your binary\n# 5 local devices\n# [...]</code></pre>\n<p>And now for the last big piece: the bootable image. Just as with the Go box, we re-use the tine catalog’s package manager that gets packages from Fedora Rawhide. This is the minimum set to be able to boot in a virtual machine, plus bash. As an extra ops (operation) this installs the built hello binary from the above go rule.</p>\n<pre><code>image.bootable_disk(\n    name = &quot;demo&quot;,\n    package_manager = &quot;tine//catalog:fedora.rawhide.package-manager&quot;,\n    definitions = image.DEFAULT_USR_VERITY_PARTITIONS,\n    version = &quot;0.0.0&quot;,\n    package_sets = [&quot;bootable&quot;],\n    packages = [&quot;bash&quot;],\n    ops = [\n        image.copy(&quot;:duf[duf]&quot;, &quot;/usr/bin/duf&quot;),\n    ],\n)</code></pre>\n<p>We can ask Buck2 for all available build targets in a cell.</p>\n<pre><code>tine/bin/tine buck targets //...\n# root//:demo\n# root//:demo.initrd\n# root//:duf\n# root//:duf.git\n# root//:example.git\n# root//:go.box\n# root//:go.box.exec</code></pre>\n<p>After all of that, we can now build the image. However, it’s far more interesting to actually see it live in QEMU. Let’s add a VM definition, with auto-login for convenience, that boots our shiny new demo image using QEMU and related tools from tine’s own Rawhide catalog.</p>\n<pre><code>image.vm(\n    name = &quot;demo-vm&quot;,\n    autologin = &quot;root&quot;,\n    # re-using tine&#39;s rawhide box which has all of QEMU etc. installed\n    box = &quot;tine//catalog:fedora.rawhide.box&quot;,\n    image = &quot;:demo&quot;,\n    credentials = {\n        &quot;firstboot.timezone&quot;: &quot;UTC&quot;,\n    }\n)</code></pre>\n<p>This single command from a clean tree will then build the Go project, the image, and boot it.</p>\n<pre><code>tine/bin/tine buck run :demo-vm</code></pre>\n<p>You should see the following.</p>\n<pre><code>[  OK  ] Reached target graphical.target - Graphical Interface.\nFedora Linux 46 (Rawhide Prerelease)\nKernel 7.3.0-0.rc3.260916g9b87fdc9af2f.34.fc46.x86_64 on an x86_64 (hvc0)\nfedora login: root (automatic login)\n-bash-5.3# duf --help\nUsage of duf:\n      --all   include pseudo, duplicate, inaccessible file systems\n[...]\n-bash-5.3# ...\n-bash-5.3# systemctl poweroff</code></pre>\n<p>Look at tine’s examples/ directory for BUCK and auxiliary files for various scenarios such as a SecureBoot/verity OS signed by either a generated or hardware key, how to build Go/Rust projects, the various kinds of rules and customizations which tine offers, or how to write integration tests.</p>\n<h2 id=\"sboms-for-free\">SBOMs for Free</h2>\n<p>tine gives you a lot more for free. For example, it integrates the Syft SBOM generator so you can ask it to build a CycloneDX standard SBOM. Let’s also specify an output path, so that you don’t have to fish it out of buck-out/:</p>\n<pre><code>tine/bin/tine buck build --out /tmp/demo.cdx.json :demo[sbom][cyclonedx]</code></pre>\n<h2 id=\"there-s-much-more\">There’s Much More!</h2>\n<p>This blog post has only covered the basics. In order to get a fuller picture, please refer to the documentation. We think the following topics are the most helpful to get started.</p>\n<ul><li>The distro package machinery for importing, modifying, and syncing/merging packages from Fedora or Arch</li><li>Setting up a shared build cache, and the details of its threat model and design</li><li>Signing builds with hardware keys through PKCS#11</li><li>Including auditable cargo projects and Go projects into image builds</li><li>Using tine mount for rapid iteration on a built third-party git project component</li><li>tine comes with builtin tools for bumping dependencies and refreshing the catalog which you can re-use in your project to automate the regular housekeeping and get tested PRs for them, like this one</li><li>tine also supports Arch, BTW!</li></ul>\n<p>The next post in this series will be about the update and provisioning system we have built to distribute these and other images. We hope to see you then!</p>\n<p>This post is part of a series covering some of the open source work we have been doing in recent months. Today we introduce and publish <em>tine</em>, our new Buck2-based build system.</p>\n<h2 id=\"our-requirements-2\">Our Requirements</h2>\n<p>Building an operating system with cryptographically verifiable integrity has to start with a build system with these very properties. At the same time, we are part of the greater open source community and want both to contribute and re-use as much existing work as possible. We also aim for efficient development with fast turnaround.</p>\n<p>This roughly translates into the following requirements for our build system:</p>\n<ul><li><strong>Minimal host requirements.</strong> It must be self-contained and must minimize external dependencies, so that it can be run in any environment.</li><li><strong>Full control over inputs.</strong> It must allow pinning every piece of software that goes into a product. It must support a choice of upstream distributions (Fedora, CentOS, Arch, Debian, etc.) and reuse existing packages where possible, while still making it easy to react quickly to CVEs and to diverge (either temporarily or permanently) from upstream packaging decisions when necessary.</li><li><strong>Integrated package machinery.</strong> It must provide tooling for importing, updating, and merging imported packages.</li><li><strong>Cheap world rebuilds.</strong> It must be able to rebuild the world on demand, e.g. after a gcc bump.</li><li><strong>Hermetic, reproducible builds.</strong> All component and image builds must run in a hermetic environment and produce bitwise reproducible output.</li><li><strong>Native image builds.</strong> It must be able to build bootable operating system images and systemd sysext images natively and concurrently.</li><li><strong>Monorepo-based iteration.</strong> It must support maintaining the operating system in a single top-level monorepo for fast end-to-end iteration. A change to an imported rpm or to a Go or Rust component must be immediately buildable and testable across the full set of images, without intermediate commits or pushes and without elaborate version or dependency declarations.</li><li><strong>First-class custom components.</strong> It must natively and efficiently build Go and Rust projects from pinned external repositories, for example kubernetes or varlink-http-bridge.</li><li><strong>Scanner compatibility.</strong> Built images must work with standard SBOM tooling and security scanners such as syft/grype or trivy.</li><li><strong>Caching.</strong> Builds must be able to retrieve unchanged components from a local and/or global cache. Building everything from scratch can take hours, and a developer is usually only working on a single component.</li></ul>\n<h2 id=\"existing-tools-2\">Existing Tools</h2>\n<p>Before building our own build tool, we evaluated several options.</p>\n<h3 id=\"mkosi-2\">mkosi</h3>\n<p>Given our team includes the creator and maintainer of <code>mkosi</code>, it was a natural first candidate to evaluate. But we quickly came to the conclusion that it has some fundamental shortcomings. It’s great at building individual images based on upstream packages, but becomes restrictive when building several weakly related images or if you need more control over the artifacts that make up an image. Building multiple images is limited to images that are intended to be shipped as part of the “main” image.</p>\n<p>We need to build many different kinds of artifacts in a uniform and robust way, not just images. Hence the build needs to be orchestrated by a generic and flexible tool. The main build file should be a language calling into <em>library functions</em> like “compile a cargo crate” or “build a UKI”.  mkosi is the opposite, it’s a <em>framework</em>: It knows how to build images, and only gives you free-form opaque hooks for the other kinds of builds. That leads to a bad experience when you want to build more than just images.</p>\n<h3 id=\"open-build-service-2\">Open Build Service</h3>\n<p>The Open Build Service (OBS) is a powerful fully-integrated build system that is primarily used by SUSE and the openSUSE project to produce all of their artifacts, anything from packages to ISOs, and many other image formats. It has strong dependency tracking and supports a dizzying array of distributions.</p>\n<p>However, it’s also the antithesis of “minimal host requirements”. The server side of it is required, central, and non-trivial to self-host. It’s also not a generic build system, meaning any new artifact types would either have to be modelled as packages or require heavy patches to OBS. We concluded that this lack of flexibility combined with its overall architecture would make it difficult for OBS to meet our requirements.</p>\n<h3 id=\"buildstream-2\">BuildStream</h3>\n<p>Apache BuildStream describes the operating system image as a graph of YAML &quot;elements&quot;, each with its own sources, dependencies and build commands. BuildStream builds each one in a bubblewrap sandbox and caches the result under a hash of everything that went into it, similar to Buck2. It is mature and used to build freedesktop-sdk, GNOME OS, and WebKitGTK.</p>\n<p>Our concerns with BuildStream are mostly around bootstrapping and extensibility. BuildStream is a Python application with compiled extensions and other dependencies. It relies on a separate set of helper programs, plus sandboxing tools from the host. Each of those can be pinned, but through different mechanisms, and even then the result still depends on the host&#39;s Python. In practice you run it from a pinned container image instead, but then you’re still dependent on an entire container runtime you don’t control.</p>\n<p>BuildStream’s YAML is a plain data format, and is extended with Python plugins. YAML has no functions, so over time you end up copy-pasting across the project. Ultimately we decided to go for a tool with a better bootstrapping and pinning story as well as a more flexible language.</p>\n<h3 id=\"antlir-2\">Antlir</h3>\n<p>Antlir is Meta&#39;s OS image builder, built on top of the Buck2 build system engine. Buck2 is Meta&#39;s open source build system with emphasis on correctness, flexibility, and caching as much as possible. Antlir implements various rules for building images with Buck2.</p>\n<p>As Antlir is a high-level tool focused on Meta’s internal repository, it is naturally very opinionated and designed for Meta’s internal use cases. For example, it requires btrfs and is strongly focused on a single monorepo.</p>\n<p>While we decided against using Antlir itself, its underlying engine, Buck2, turned out to be a good fit, and we ended up choosing it as the foundation for our own build system.</p>\n<h2 id=\"our-build-system-tine-2\">Our Build System: tine</h2>\n<p>In essence, tine is a set of opinionated Buck2 rules to build rpm, Rust crate and Go module components, UKIs, and images; it can sign images either with a hardware key through PKCS#11 or a locally generated key. The intent is to combine the best ideas from Antlir and mkosi into a single tool.</p>\n<p>tine has only three requirements on its build host: git, python3 (just for its own bootstrapping, not for production builds) and user namespaces. From there, it bootstraps everything it needs from pinned declarations to get a reproducible and independent build environment. That can be a distribution as old or modern as you need.</p>\n<p>An important tine concept is the “<strong>box</strong>”, which is a declared and pinned down environment to run a build task. Think containers or distrobox, but declared natively in Buck2’s language, and using Buck2’s caching and rebuild rules, so they build quickly and naturally, stay reproducible, and need no further dependencies to run. tine itself defines boxes for running rpmbuild, go, or cargo, or a bigger multi-purpose one called fedora.rawhide.box which contains e.g. systemd-ukify for building images and QEMU for running virtual machines. Your own project can define its own boxes.</p>\n<h3 id=\"tine-s-engine-buck2-2\">tine’s Engine: Buck2</h3>\n<p>To better understand this post and the examples, here is a one-minute Buck2 primer for those familiar with Make or Meson:</p>\n<ul><li><strong>Build file:</strong> A BUCK file is the directory&#39;s Makefile equivalent. It&#39;s written in a Python dialect called Starlark, and declares all<em>targets</em> that you can build.</li><li><strong>Cell:</strong> A named build graph root; these roughly follow the boundaries of git repositories: // is your own top-level project (the OS you want to build), tine// is the tine checkout which your project pulls in.</li><li><strong>Target:</strong> A named node in the build graph, i.e. one particular thing that you want to build. They are addressed with an absolute path of the form cell//directory/sub:name, which refers to a target name defined in cell’s directory/sub/BUCK file. Within a cell, you can also use relative paths, like<code>subdir:name</code> , or even just<code>:name</code> for a target in the current directory. A single target can publish several output variants (“subtargets”), e.g.<code>:my_cool_os[qcow2]</code> or<code>:my_cool_os[sbom]</code> .</li><li><strong>Rule:</strong> The equivalent of a meson<code>*_target()</code> , or the structure of a Makefile rule: a Starlark expression which translates a target into a set of<em>actions</em> and their parameters. It does not run anything by itself. For example, a<code>bootable_disk(name = “myos”, param1 = …)</code> rule defines a<code>myos</code> target and invokes a<code>bootable_disk</code> rule which translates it into actions like “install rpms”, “run<code>systemd-repart”</code> and so on.</li><li><strong>Action:</strong> One build command with its declared inputs and outputs, the equivalent of the commands in a Makefile rule. That abstraction allows running all of them consistently in a sandbox which only sees these inputs. When Starlark doesn’t suffice, these rules can be implemented with the full power of Python.</li></ul>\n<p>More information can be found on Buck2’s key concepts page.</p>\n<p>Unlike Make or Meson, Buck2 never decides what to rebuild from timestamps. An action is keyed by a hash of all of its inputs: the sources, the tool binaries, the build platform configuration, and the command line itself. That is what makes its incremental builds correct and trustworthy, and it also allows taking an action&#39;s result from a shared cache instead of re-running it.</p>\n<h2 id=\"walkthrough-building-a-bootable-image-2\">Walkthrough: Building a Bootable Image</h2>\n<p>Let’s walk through how to use tine in your own projects. We will build a very basic example from scratch: a bootable image based on Fedora Rawhide with a Go project, and boot it. In this example, we’ll use duf, a CLI tool that shows free/used disk space in a text terminal with nice ASCII art.</p>\n<p>Let’s follow tine&#39;s README and set up a fresh demo git repository which pulls in tine and initializes it.</p>\n<pre><code>git init tine-demo\ncd tine-demo\ngit submodule add https://github.com/amutable-systems/tine tine\ntine/bin/tine init\ngit add .\ngit commit -m &quot;initialize&quot;</code></pre>\n<p>Now let’s add a BUCK file. We’ll walk through it in several blocks, but these all go in the same file. First we need to import some definitions. This is Starlark, so akin to Python’s import statements.</p>\n<pre><code>load(&quot;@tine//box:defs.bzl&quot;, &quot;box&quot;)\nload(&quot;@tine//git:defs.bzl&quot;, &quot;git&quot;)\nload(&quot;@tine//go:defs.bzl&quot;, &quot;go&quot;)\nload(&quot;@tine//image:defs.bzl&quot;, &quot;image&quot;)</code></pre>\n<p>Next we need to define a build environment for the Go compiler. tine already offers a Fedora rawhide catalog. So, let’s just use that (hence the tine// cell) and Fedora’s golang package. A real project would likely define and track their parent OS catalog by itself, instead of blindly following tine’s.</p>\n<pre><code>box.new(\n    name = &quot;go.box&quot;,\n    packages = [&quot;golang&quot;],\n    release = &quot;tine//catalog:fedora.rawhide.release&quot;,\n)</code></pre>\n<p>Declare the duf Go project git repository which we want to build. tine requires pinning every input exactly, so we specify a git commit ID. That git repository is then passed as input to the go.package() rule which binds the above go.box and the git checkout, both referenced as relative targets (see above), hence the colon separator.</p>\n<pre><code>git.fetch(\n    name = &quot;duf.git&quot;,\n    repo = &quot;https://github.com/muesli/duf&quot;,\n    rev = &quot;4636deb4a7b707a9f04c602db033f9837e50b3f6&quot;,\n)\ngo.package(\n    name = &quot;duf&quot;,\n    box = &quot;:go.box&quot;,  # a target in the current directory\n    src = &quot;:duf.git&quot;, # another target\n)</code></pre>\n<p>With that we can already build and execute the binary.</p>\n<pre><code>tine/bin/tine buck run :duf\n# [...]\n# BUILD SUCCEEDED - starting your binary\n# 5 local devices\n# [...]</code></pre>\n<p>And now for the last big piece: the bootable image. Just as with the Go box, we re-use the tine catalog’s package manager that gets packages from Fedora Rawhide. This is the minimum set to be able to boot in a virtual machine, plus bash. As an extra ops (operation) this installs the built hello binary from the above go rule.</p>\n<pre><code>image.bootable_disk(\n    name = &quot;demo&quot;,\n    package_manager = &quot;tine//catalog:fedora.rawhide.package-manager&quot;,\n    definitions = image.DEFAULT_USR_VERITY_PARTITIONS,\n    version = &quot;0.0.0&quot;,\n    package_sets = [&quot;bootable&quot;],\n    packages = [&quot;bash&quot;],\n    ops = [\n        image.copy(&quot;:duf[duf]&quot;, &quot;/usr/bin/duf&quot;),\n    ],\n)</code></pre>\n<p>We can ask Buck2 for all available build targets in a cell.</p>\n<pre><code>tine/bin/tine buck targets //...\n# root//:demo\n# root//:demo.initrd\n# root//:duf\n# root//:duf.git\n# root//:example.git\n# root//:go.box\n# root//:go.box.exec</code></pre>\n<p>After all of that, we can now build the image. However, it’s far more interesting to actually see it live in QEMU. Let’s add a VM definition, with auto-login for convenience, that boots our shiny new demo image using QEMU and related tools from tine’s own Rawhide catalog.</p>\n<pre><code>image.vm(\n    name = &quot;demo-vm&quot;,\n    autologin = &quot;root&quot;,\n    # re-using tine&#39;s rawhide box which has all of QEMU etc. installed\n    box = &quot;tine//catalog:fedora.rawhide.box&quot;,\n    image = &quot;:demo&quot;,\n    credentials = {\n        &quot;firstboot.timezone&quot;: &quot;UTC&quot;,\n    }\n)</code></pre>\n<p>This single command from a clean tree will then build the Go project, the image, and boot it.</p>\n<pre><code>tine/bin/tine buck run :demo-vm</code></pre>\n<p>You should see the following.</p>\n<pre><code>[  OK  ] Reached target graphical.target - Graphical Interface.\nFedora Linux 46 (Rawhide Prerelease)\nKernel 7.3.0-0.rc3.260916g9b87fdc9af2f.34.fc46.x86_64 on an x86_64 (hvc0)\nfedora login: root (automatic login)\n-bash-5.3# duf --help\nUsage of duf:\n      --all   include pseudo, duplicate, inaccessible file systems\n[...]\n-bash-5.3# ...\n-bash-5.3# systemctl poweroff</code></pre>\n<p>Look at tine’s examples/ directory for BUCK and auxiliary files for various scenarios such as a SecureBoot/verity OS signed by either a generated or hardware key, how to build Go/Rust projects, the various kinds of rules and customizations which tine offers, or how to write integration tests.</p>\n<h2 id=\"sboms-for-free-2\">SBOMs for Free</h2>\n<p>tine gives you a lot more for free. For example, it integrates the Syft SBOM generator so you can ask it to build a CycloneDX standard SBOM. Let’s also specify an output path, so that you don’t have to fish it out of buck-out/:</p>\n<pre><code>tine/bin/tine buck build --out /tmp/demo.cdx.json :demo[sbom][cyclonedx]</code></pre>\n<h2 id=\"there-s-much-more-2\">There’s Much More!</h2>\n<p>This blog post has only covered the basics. In order to get a fuller picture, please refer to the documentation. We think the following topics are the most helpful to get started.</p>\n<ul><li>The distro package machinery for importing, modifying, and syncing/merging packages from Fedora or Arch</li><li>Setting up a shared build cache, and the details of its threat model and design</li><li>Signing builds with hardware keys through PKCS#11</li><li>Including auditable cargo projects and Go projects into image builds</li><li>Using tine mount for rapid iteration on a built third-party git project component</li><li>tine comes with builtin tools for bumping dependencies and refreshing the catalog which you can re-use in your project to automate the regular housekeeping and get tested PRs for them, like this one</li><li>tine also supports Arch, BTW!</li></ul>\n<p>The next post in this series will be about the update and provisioning system we have built to distribute these and other images. We hope to see you then!</p>","headings":[{"level":1,"text":"From Thin Air to Bootable Images: The tine Build System","id":"from-thin-air-to-bootable-images-the-tine-build-system"},{"level":2,"text":"Table of contents","id":"table-of-contents"},{"level":2,"text":"Our Requirements","id":"our-requirements"},{"level":2,"text":"Existing Tools","id":"existing-tools"},{"level":3,"text":"mkosi","id":"mkosi"},{"level":3,"text":"Open Build Service","id":"open-build-service"},{"level":3,"text":"BuildStream","id":"buildstream"},{"level":3,"text":"Antlir","id":"antlir"},{"level":2,"text":"Our Build System: tine","id":"our-build-system-tine"},{"level":3,"text":"tine’s Engine: Buck2","id":"tine-s-engine-buck2"},{"level":2,"text":"Walkthrough: Building a Bootable Image","id":"walkthrough-building-a-bootable-image"},{"level":2,"text":"SBOMs for Free","id":"sboms-for-free"},{"level":2,"text":"There’s Much More!","id":"there-s-much-more"},{"level":2,"text":"Our Requirements","id":"our-requirements-2"},{"level":2,"text":"Existing Tools","id":"existing-tools-2"},{"level":3,"text":"mkosi","id":"mkosi-2"},{"level":3,"text":"Open Build Service","id":"open-build-service-2"},{"level":3,"text":"BuildStream","id":"buildstream-2"},{"level":3,"text":"Antlir","id":"antlir-2"},{"level":2,"text":"Our Build System: tine","id":"our-build-system-tine-2"},{"level":3,"text":"tine’s Engine: Buck2","id":"tine-s-engine-buck2-2"},{"level":2,"text":"Walkthrough: Building a Bootable Image","id":"walkthrough-building-a-bootable-image-2"},{"level":2,"text":"SBOMs for Free","id":"sboms-for-free-2"},{"level":2,"text":"There’s Much More!","id":"there-s-much-more-2"}]}}