Step 3: Joining Buildroot and Buck2¶
Step 1 showed what Buildroot builds; Step 2 showed how Buck2 decides what to run. buckroot joins them: it reads a Buildroot project and generates a Buck2 graph from it, one target per package, so that Buck2 can build, cache and distribute a Buildroot image. This page runs the commands that do the joining, and explains what each one does under the hood.
All commands run from experiments/helloworld. Every output is real.
The one input: project.json¶
buckroot needs to be told which Buildroot project you mean. That is one small file:
{
"name": "helloworld",
"defconfig": "helloworld_qemu_aarch64_musl_rootfs_defconfig",
"external": true,
"buildroot": {"repo": "https://git.buildroot.net/buildroot", "ref": "2025.02.18"},
"native": ["hello", "host-skeleton", "host-fakedate", "skeleton", "musl-compat-headers",
"initscripts", "urandom-scripts", "skeleton-init-sysv", "skeleton-init-common",
"toolchain", "toolchain-external", "toolchain-external-bootlin"]
}
| Key | What it says |
|---|---|
name |
what to call the project: it names the cache directory ~/.cache/br2/helloworld and the Buildbarn instance |
defconfig |
the Buildroot configuration to build (a file from the external tree's configs/, see Step 1) |
external |
there is a BR2_EXTERNAL tree (your own packages) in the directory buildroot-external/ |
buildroot |
which Buildroot to download: a release, by tag |
native |
a list used only by the native variant: which packages Buck2 should build itself, without make (see Step 6) |
Everything else in an experiment directory has a fixed name and meaning: buildroot-src/ is the downloaded Buildroot,
buildroot-external/ your packages, common/ local sources, golden/ the reference results.
Installing the tools into the project: br2-sync¶
../../toolkit/bin/br2-sync .
buckroot's code (the Buck2 rules, the scripts, the br2 command) lives in toolkit/. br2-sync copies the parts an
experiment needs into it (scripts/, br2/, platforms/, toolchains/, infra/) and writes two files: .buckconfig
(which tells Buck2 where the project root is and which rules to use) and a small top-level BUCK. The copies are ignored
by git, so an experiment directory contains the version of the toolkit it was run with, and Buck2 sees the rules as
part of its own project.
br2 setup: fetch what is needed¶
scripts/br2 setup
Fetched https://git.buildroot.net/buildroot 2025.02.18 (d030e36bbc9669230c015be971b14b6e062cfdde) into .../buildroot-src
Two things happen, and both are safe to repeat:
- The
buck2program itself is downloaded intotools/(from a shared cache in~/.cache/br2/tools, so a second project does not download it again). Every experiment pins the same version. - Buildroot is fetched with
git clone --depth 1 --branch 2025.02.18intobuildroot-src/, and the exact commit is written toBUILDROOT_PINNED_COMMIT.txt.
Nothing has been built. buildroot-src/ is a plain Buildroot tree that nothing will ever write to again: from here on it
is mounted read-only into every build step.
br2 extract: from a configuration to a graph¶
scripts/br2 extract
+ scripts/br2buck.py extract
29 packages -> golden/model.json
+ scripts/br2buck.py render
33 files rendered
This is the step that turns "a Buildroot configuration" into "a Buck2 graph". It has two halves.
Half one: ask Buildroot what the project contains¶
To build a graph, buckroot needs to know which packages the configuration includes and which depend on which. Buildroot
knows: it has a command, make show-info, that prints exactly that as JSON. So extract runs Buildroot (but does not
build anything) in a scratch directory:
make helloworld_qemu_aarch64_musl_rootfs_defconfig: turn the defconfig into a full.config.- Append the two settings every buckroot build needs,
BR2_PER_PACKAGE_DIRECTORIES=yandBR2_REPRODUCIBLE=y, thenmake olddefconfigto let Buildroot settle the result. make show-info: ask for every package.make show-vars VARS=GNU_TARGET_NAME: ask for the toolchain's name (aarch64-buildroot-linux-musl), which names the sysroot directory.
That takes about a minute and writes golden/model.json, the model: only the graph - each package's version, its
direct dependencies, its source with checksums, the directory where its makefile lives, and the stamp directory Buildroot
uses. This is the entry for hello:
"hello": {
"deps": ["host-fakedate", "host-skeleton", "skeleton", "toolchain"],
"dir": "buildroot-external/package/hello",
"dl_dir": "hello",
"kind": "target",
"sources": [{"file": "hello-1.0.tar.gz", "local": "common/hello"}],
"stamp_dir": "build/hello-1.0",
"version": "1.0",
"virtual": false
}
deps is Buildroot's own answer to "what has to exist before hello can be built?". kind is target (runs on the
device) or host (runs on the build machine). sources says where the code comes from. A package with a real
download looks like this (host-e2fsprogs, shortened):
"host-e2fsprogs": {
"deps": ["host-fakedate", "host-pkgconf", "host-skeleton", "host-util-linux"],
"dir": "buildroot-src/package/e2fsprogs",
"kind": "host",
"sources": [{
"file": "e2fsprogs-1.47.2.tar.xz",
"sha256": "08242e64ca0e8194d9c1caad49762b19209a06318199b63ce74ae4ef2d74e63c",
"urls": ["https://cdn.kernel.org/pub/linux/kernel/people/tytso/e2fsprogs/v1.47.2/e2fsprogs-1.47.2.tar.xz", "..."]
}],
"stamp_dir": "build/host-e2fsprogs-1.47.2",
"version": "1.47.2"
}
Notice what is not in the model: how to build anything. The build commands stay in Buildroot's makefiles, where they belong. buckroot takes the graph from Buildroot and lets Buildroot keep doing the building.
Half two: write the BUCK files¶
render reads only the model (no Buildroot needed) and writes one BUCK file for every directory that holds packages, so
the Buck2 graph has the same shape as Buildroot's tree. For hello it wrote buildroot-external/package/hello/BUCK. If
Step 2 made BUCK files familiar, this is the same thing, generated:
br2_inputs( # (1)
name = "inputs",
srcs = ["Config.in", "hello.mk"],
visibility = ["PUBLIC"],
)
br2_package( # (2)
name = "hello",
pkg = "hello",
pkg_inputs = ":inputs",
stamp_dir = "build/hello-1.0",
dl_dir = "hello",
deps = [ # (3)
("//br2/native/host-fakedate:host-fakedate" if "host-fakedate" in read_config("br2", "native", "").split(",")
else "//buildroot-src/package/fakedate:host-fakedate"),
...
("//br2/native/toolchain:toolchain" if "toolchain" in read_config("br2", "native", "").split(",")
else "//buildroot-src/toolchain/toolchain:toolchain"),
],
local_srcs = ["//common/hello:src"], # (4)
visibility = ["PUBLIC"],
)
br2_inputslists every file in the package's directory: an input ofhello's actions, and of every package that depends onhello. Edithello.mkand they are invalidated.br2_packageis the rule that runs Buildroot'smake helloinside an action. The target's label is//buildroot-external/package/hello:hello.- The dependencies are the model's
deps, as Buck2 labels. Each is a conditional: if the package is on thenativelist, the dependency is its native target, otherwise the normal one. That singleread_configis the whole switch between the variants, per dependency. local_srcsdeclarescommon/helloas an input, becausehello's source is a directory, not a download.
The top-level BUCK aliases //:rootfs to //br2/generated:rootfs. The file br2/generated/BUCK defines three
targets: config (the .config for the defconfig), owners (which config option belongs to which package
directory; used to give each package only the options it can see), and rootfs (the final image, which depends on all 29
packages). So:
$ buck2 build //:rootfs # build the whole image, every package in dependency order
$ buck2 uquery 'kind(br2_package, deps(//:rootfs))' | wc -l
29
That is the graph: 29 package targets, the same 29 Buildroot lists. What is left is what runs inside each target's action, and that is what the three variants differ on.
br2 golden: the answer key¶
scripts/br2 golden
Before Buck2 builds anything, buckroot builds the same project with plain Buildroot (no Buck2) and keeps the result. It takes a few minutes. It runs in the same fixed-path environment as the Buck2 build (explained on the next page) with the same configuration. This golden build is the reference: the definition of "correct".
To compare images, buckroot reduces one to a manifest: for every path in the image, its type, mode, owner and (for files)
size and SHA-256. scripts/fs-manifest.py scan writes golden/rootfs.manifest.json, 369 paths for helloworld:
"/etc/hostname": {"gid": 0, "mode": "0644", "sha256": "b75d7f28...", "size": 10, "type": "file", "uid": 0}
From now on, "Buck2 got it right" means one thing: the manifest of the Buck2-built image is IDENTICAL to this one, path for path and hash for hash. Why a manifest and not the raw image bytes is ADR-0006: filesystem images contain incidental details (inode numbers, allocation order) that say nothing about the contents.
What you have now¶
golden/model.json: the package graph, extracted from Buildroot;BUCKfiles: the graph as Buck2 targets (//:rootfsand one target per package);golden/rootfs.manifest.json: the answer key;- tools and Buildroot fetched, but nothing yet built by Buck2.
The next three pages fill in the same question: what runs inside a package's action?
| Step 4: Wrapped | Step 5: Stepped | Step 6: Native | |
|---|---|---|---|
| One action does | all of make hello |
one Buildroot step | one small job (compile, link, install) |
| Buildroot's makefile runs | yes, whole | yes, one step at a time | no |
| Works for | every package | every package | packages that have a rule |
| Skips work when | the package's inputs are unchanged | a step's inputs are unchanged | a job's inputs are unchanged |
| Status | works; used for every project | design only | works for a set of packages |