Views and config slices¶
Two mechanisms make a package's Buck2 cache key depend only on what the package can
actually use: an enforced view of the Buildroot tree, and a slice of the
configuration. Without them every action would declare the whole tree and the whole
.config, and any change - a new package, one defconfig line - would rerun every
action.
Views¶
Buck2 runs local actions unsandboxed, so narrowing an action's declared inputs is unsound on its own: the action could still read the whole checkout, and a change to a file it read but did not declare would be a stale cache hit. buckroot therefore narrows what the action sees.
Every package action runs make inside the private mount namespace of
br2-ns.sh. Inside it, /mnt/src (the Buildroot tree) and /mnt/external (the
BR2_EXTERNAL tree) are built from an allow-list:
- infrastructure - every entry of the tree except
package/<dir>/, stray dot-config files,.git,outputanddl; - the package directories of the package's dependency closure and of the package itself;
- links and extra view entries, below.
Each is bind-mounted read-only. An undeclared read is ENOENT, never a stale hit. The
same mechanism is what makes remote execution sound, because a remote worker only has
the declared inputs anyway.
make parses with a subset of packages because Buildroot's top-level makefile does
include $(sort $(wildcard package/*/*.mk)): a package that is not in the view is
simply not seen. The dependencies of a package, which are all make walks, are.
What the enforced view finds¶
Enforcement immediately exposes couplings Buildroot never declared:
busybox'sS02sysctlis a symlink into../procps-ng/, a directory outside busybox's closure.- Group directories (
package/gcc/,package/opengl/) whose.mkis included by a makefile in the parent directory: the parent and its non-package subdirectories (patch directories) must be visible too. The renderer adds ancestors of directories with a group makefile automatically. - Patches and hashes in directories the package itself does not own - the reason
for
br2 preflight. .mkfiles that read another package's directory (gettext-tiny reading gettext-gnu's patches, a rust package readingpackage/rustc): declared per project withextra_view.- Dangling symlinks in the source tree: Buck2 cannot see them as inputs, so the renderer
records them as data (
path -> target) and the action recreates them in an overlay.
scripts/check-narrow.py finds the rest by construction, comparing every make variable
between the full tree and the view.
.git is hidden¶
Buildroot asks git for BR2_VERSION_FULL (-dirty depends on the state of the
worktree) and for SOURCE_DATE_EPOCH. The namespace mounts an empty directory over
.git in both trees, so both fall back to fixed values from the Makefile and results do
not depend on who ran git status last. For a BR2_EXTERNAL tree a version string
embedded from its git state is hidden the same way.
Config slices¶
The .config is an input to every package, and it is the coarsest one: any change to it
would rerun everything, and each action ran Kconfig's syncconfig over the whole tree.
Ownership. br2_owners scans every Config.in* under package/ and records which
directory declares each BR2_* symbol. Of the 8,900 symbols a typical Buildroot tree
declares, roughly 2,900 are in a real .config and about 1,700 of those belong to a
package; the rest are global (architecture, toolchain, system, filesystem options).
Slice. A package's slice contains the .config lines whose symbol is
- global (its owner is not under
package/), or - owned by the package's directory, its ancestors, or the directories of its dependency closure, or
- mentioned in the package's own
.mkfiles (\bBR2_[A-Z0-9_]+; a trailing_makes it a prefix), or - referenced by the infrastructure makefiles (a couple of dozen package symbols,
such as
BR2_PACKAGE_BASH), or - a
BR2_PACKAGE_PROVIDES_*/BR2_PACKAGE_HAS_*symbol: the choice of which package implements a virtual package (libgles,libegl, ...) is read by whichever package is built on it.
Dropped lines belong to packages this one cannot depend on. Adding
BR2_PACKAGE_STRACE=y flips one line that is in nobody else's slice.
Two details make the slices stable:
- Only
BR2_X=valuelines are kept.# BR2_X is not setlines and Kconfig comment blocks appear and disappear with the visibility of unrelated options, and leaked into every slice on the first attempt. - The slice ships its own
auto.conf.auto.confis read only by the Makefile'spreparestep, so seeding it makespreparea no-op: nosyncconfig, no Kconfig, noConfig.infiles and noconfbinary in a package action.
The slice is a deterministic tar (sorted, fixed mtime and owner), so an unchanged slice has an unchanged digest and Buck2's early cutoff stops the invalidation: a config change reruns about thirty tiny slice actions, but a package rebuilds only if its slice changed.
Deliberately conservative¶
All global symbols go to every package. Changing the optimization level or the architecture correctly rebuilds everything. Splitting them - for example, symbols that only the rootfs uses - is a possible refinement, not a soundness need.
Known limits¶
- Global config symbols and edits to any infrastructure file still rebuild everything.
- Slicing is static analysis, validated by
check-narrow.pyfor the current configuration; rerun it after adding packages. - The ambient host toolchain (gcc, perl, rsync, make) is not part of any key: see Wrapped and native.
- The mount namespace needs
CAP_SYS_ADMIN.