Skip to main content

TECH VEDA

Embedded Linux on Edge-AI 23rd Sept 2026 enrollingLinux kernel & Device drivers starts on 24th Oct 2026 enrollingCorporate on-site training - Submit proposal Pick your modulesSharpen your kernel skills: deep dives, drivers, Yocto, CVEs, careers — updated daily. Read the blog →Embedded Linux fast track starts 23rd sept 2026 enrollingEmbedded Linux Mastery track starts 23rd sept 2026 enrollingLinux systems engineering starts 23rd sept 2026 enrolling
Tutorials

Buildroot for Embedded Linux — Part 2: Adding Packages and a BR2_EXTERNAL Tree

Set up a Buildroot BR2_EXTERNAL tree so your defconfig and board files live outside the Buildroot source, then build out of tree with O=.

Buildroot for Embedded Linux — Part 2: Adding Packages and a BR2_EXTERNAL Tree

A BR2_EXTERNAL tree keeps your defconfig, board files and package recipes outside the Buildroot source directory, so upgrading Buildroot means unpacking a new release rather than merging your changes into it. The tree needs exactly three files: external.desc, external.mk and Config.in. You select it with make BR2_EXTERNAL=/path/to/tree menuconfig, and Buildroot remembers the choice in a hidden .br2-external.mk file in the output directory. Add an out-of-tree build using O= and the Buildroot source directory stays completely unmodified.

In Part 1 of this series we built a root filesystem for an ARM target from qemu_arm_vexpress_defconfig, booted it under QEMU, and saved a minimal configuration with make savedefconfig. Everything lived inside the Buildroot source directory, which is fine for one afternoon and wrong for a product. In this part we add real packages to the image, then move every file we own into a BR2_EXTERNAL tree so the Buildroot source directory contains nothing of ours, and finish by building out of tree.

What you need

The working Buildroot directory from Part 1, with a completed build. The commands here were checked against the Buildroot user manual and the package files at the 2026.05.2 tag, the final point release of the 2026.05 series that Part 1 used. Buildroot 2026.08 was released on 4 September 2026 and is now the current stable series; the package definitions quoted below and the BR2_EXTERNAL mechanism are byte-identical in it, so if you are starting fresh, use 2026.08 and follow along unchanged.

Everything below is still run as a normal user.

Adding packages to the image

Buildroot ships several thousand package definitions, so find one with the search tool inside the configurator rather than by scrolling. Open the menu and press the / key:

raghu@techveda.org:~$ make menuconfig

Type htop into the search box. The result shows the configuration symbol, the help text and the dependency lines, and it is that last part which makes the search worth using instead of browsing the Target packages menu. The definition explains most of what you need to know:

config BR2_PACKAGE_HTOP
	bool "htop"
	depends on BR2_USE_MMU # fork()
	depends on !BR2_STATIC_LIBS # dlopen()
	select BR2_PACKAGE_NCURSES

Two mechanisms appear here and they behave very differently. select pulls another package in automatically: enabling htop enables ncurses whether you asked for it or not, and ncurses ends up in your image. depends on does the opposite — it hides the option entirely when the condition is false. On a static-only toolchain htop is simply not offered, and no error message says why. That is the most common reason an engineer reports that “the package is not in Buildroot”. It is there; the configuration you already chose removed it from the menu.

Now add dropbear, so the target has an SSH server. Its definition contains a default that catches people out:

config BR2_PACKAGE_DROPBEAR_SMALL
	bool "optimize for size"
	default y

Because “optimize for size” defaults to on, the parent package does not select zlib or libtomcrypt, and the blowfish cipher is disabled. Turn that option off and dropbear starts selecting zlib and libtomcrypt, and your image grows.

Before building, ask Buildroot what those two selections dragged in, then rebuild the image:

raghu@techveda.org:~$ make dropbear-show-depends
raghu@techveda.org:~$ make dropbear-show-recursive-depends
raghu@techveda.org:~$ make

Know the limit before you start experimenting. Buildroot does not track which package installed which file into output/target and output/staging, so removing a package is not supported without rebuilding from scratch. Deselecting htop and running make again leaves the htop binary in your image. Adding packages is cheap; taking them away is not. The image you ship should therefore come from a clean build, not from an output directory you have been adding to for three weeks.

Three per-package targets are worth knowing, along with the condition on two of them. make htop-dirclean removes the package build directory, so the next build re-extracts, re-configures, re-compiles and re-installs from scratch. make htop-rebuild and make htop-reconfigure restart from the compilation or configuration step instead — but the manual is explicit that both only make sense when you are using the OVERRIDE_SRCDIR feature or have modified a file directly inside the build directory. They are not a general “pick up my configuration change” button, which is what most people assume. None of the three regenerates the root filesystem image either; run make afterwards for that.

Why a BR2_EXTERNAL tree is necessary

Your Buildroot directory now holds a saved defconfig in configs/, and on a real board it would soon hold a kernel configuration, a root filesystem overlay, a patch directory and a post-image script too. If you keep Buildroot under version control, which most teams do, ask git what that looks like from Buildroot’s side:

raghu@techveda.org:~$ git status --short
?? configs/techveda_vexpress_defconfig

Those files are yours, sitting inside a tree that upstream also changes. The failure mode is not dramatic, which is why it is easy to walk into. Six months later you upgrade to a newer Buildroot release. If you tracked Buildroot in git, you now resolve merge conflicts in a tree where you understand perhaps two per cent of the files. If you unpacked a tarball instead, you copy your files across by hand and find out a year later that one of them was never copied, and that the board you shipped was built from a different overlay than the one in your repository. Either way the cost is paid at upgrade time, when you are least able to absorb it.

A BR2_EXTERNAL tree removes that cost by inverting the relationship: instead of your files living inside Buildroot, Buildroot is told where your files live. The source directory stays exactly as it was unpacked, so an upgrade is unpacking a new tarball and pointing it at the same BR2_EXTERNAL tree. This is the same principle as keeping a product’s kernel changes upstreamable rather than accumulating them locally, covered in Architecting Mainline-Friendly Products.

Building a BR2_EXTERNAL tree from scratch

A BR2_EXTERNAL tree must contain at least three files. Create the directory alongside Buildroot, not inside it:

raghu@techveda.org:~$ mkdir -p ~/br2-techveda/configs
raghu@techveda.org:~$ cd ~/br2-techveda

The first file is external.desc, which names the tree. The format is one keyword per line, followed by a colon and at least one space:

name: TECHVEDA
desc: TECH VEDA vexpress board support

The name field is mandatory, may only contain characters from the set [A-Za-z0-9_], and is not cosmetic: Buildroot uses it to construct BR2_EXTERNAL_TECHVEDA_PATH, holding the absolute path of the tree, and BR2_EXTERNAL_TECHVEDA_DESC, holding the description. Both are available in Kconfig files, in makefiles, and in the environment of post-build, post-image and in-fakeroot scripts. Because you can use more than one BR2_EXTERNAL tree at a time, pick a name specific enough not to collide with a tree you might later take from a vendor. The optional desc field must fit on one line and is used as the menu prompt, so keep it to about forty characters.

The second file is external.mk, which Buildroot includes with the rest of its make logic. It may be empty, but the line worth writing now is the one that picks up package recipes as soon as you add any:

include $(sort $(wildcard $(BR2_EXTERNAL_TECHVEDA_PATH)/package/*/*.mk))

The third is Config.in, which Buildroot includes into the top-level configuration menu. Give it one real option so that there is something to see:

config TECHVEDA_FLASH_ADDR
	hex "vexpress flash address"
	default 0x10AD

There is a detail here that costs people an evening. In makefiles the variable is written $(BR2_EXTERNAL_TECHVEDA_PATH) with parentheses, as above. In Kconfig source statements it is written without them:

source "$BR2_EXTERNAL_TECHVEDA_PATH/package/mydaemon/Config.in"

Kconfig and make are different languages that happen to share a variable name here, and using the wrong sigil produces a path that does not resolve rather than an error that names the problem.

Pointing Buildroot at the BR2_EXTERNAL tree

Back in the Buildroot directory, pass the path once:

raghu@techveda.org:~$ make BR2_EXTERNAL=~/br2-techveda menuconfig

A new top-level entry appears, and with exactly one tree its contents are shown directly rather than in a submenu:

External options  --->
    *** TECH VEDA vexpress board support (in /home/raghu/br2-techveda)
    (0x10AD) vexpress flash address

You pass BR2_EXTERNAL once. Buildroot writes it into the hidden .br2-external.mk file in the output directory, and every later make there picks it up. For several trees at once, pass a space-separated list; to stop using them, pass an empty value:

raghu@techveda.org:~$ make BR2_EXTERNAL="/path/to/vendor-tree ~/br2-techveda" menuconfig
raghu@techveda.org:~$ make BR2_EXTERNAL= menuconfig

One trap produces a confusing failure. A relative BR2_EXTERNAL path is interpreted relative to the main Buildroot source directory, not to your current working directory and not to the output directory. If unsure, pass an absolute path.

Moving the defconfig into the BR2_EXTERNAL tree

Now move the configuration itself into the BR2_EXTERNAL tree:

raghu@techveda.org:~$ make savedefconfig BR2_DEFCONFIG=~/br2-techveda/configs/techveda_vexpress_defconfig
raghu@techveda.org:~$ rm configs/techveda_vexpress_defconfig

Buildroot looks in the configs/ subdirectory of every BR2_EXTERNAL tree, so the configuration is now listed alongside the built-in ones under an External configs label naming its tree, and loads with the ordinary command:

raghu@techveda.org:~$ make list-defconfigs
raghu@techveda.org:~$ make BR2_EXTERNAL=~/br2-techveda techveda_vexpress_defconfig

Note that BR2_EXTERNAL is passed on that line. On a fresh or cleaned output directory it has to be: .br2-external.mk lives in the output directory, so before that directory exists Buildroot has no way to know where your configurations are. After the first invocation you can drop it again. If the same defconfig name exists in more than one BR2_EXTERNAL tree, the last tree on the list wins, so a tree of your own can deliberately override a vendor’s configuration of the same name.

Check the result from Buildroot’s side:

raghu@techveda.org:~$ git status --short
raghu@techveda.org:~$

That empty output is the whole point of a BR2_EXTERNAL tree. Every file you own is now in ~/br2-techveda, which is the directory you put under version control, and the Buildroot directory is a pristine upstream release you can replace wholesale.

Building out of tree with O=

The source directory is clean, but the build still writes output/ and .config into it. Buildroot supports out-of-tree builds with the same O= syntax as the kernel:

raghu@techveda.org:~$ make O=/tmp/build-vexpress BR2_EXTERNAL=~/br2-techveda techveda_vexpress_defconfig

Buildroot creates the directory if it does not exist. The equivalent form, run from the output directory, is:

raghu@techveda.org:~$ cd /tmp/build-vexpress
raghu@techveda.org:~$ make O=$PWD -C ~/buildroot-2026.05.2 menuconfig

After the first invocation Buildroot generates a Makefile wrapper inside the output directory, so from then on you can simply run make there with no O= and no -C. Because the .config, the temporary files and .br2-external.mk all live in the output directory, one Buildroot source tree can serve several builds at once — a debug image and a production image, say — as long as each uses its own output directory.

The same relative-path rule applies as before: a relative O= path is interpreted relative to the Buildroot source directory, not to your current directory.

What this does not fix

A BR2_EXTERNAL tree separates your files from Buildroot’s; it does not make Buildroot incremental. Change any architecture or toolchain option and an explicit make clean is still required, and skipping it produces build failures unrelated to what you changed. It does not solve package removal either. And it version-pins nothing: record in your BR2_EXTERNAL tree which Buildroot release it was tested against, because the mechanism does not enforce that.

One note on scope: the commands above were checked against the Buildroot user manual and the package definition files at the 2026.05.2 tag, but the builds were not executed while writing this. Adjust the paths for your own directory names; the option names and file layout are as given.

Key takeaways

  • Use the / search in menuconfig to find packages, and read the select and depends on lines — depends on hides an option silently rather than reporting an error.
  • Buildroot cannot cleanly remove a package. Ship images from clean builds.
  • <pkg>-rebuild and <pkg>-reconfigure are for OVERRIDE_SRCDIR work or hand-edited build directories, not for picking up configuration changes.
  • A BR2_EXTERNAL tree needs external.desc, external.mk and Config.in; the name field creates BR2_EXTERNAL_<NAME>_PATH.
  • Pass BR2_EXTERNAL once per output directory; it is stored in .br2-external.mk. Relative paths resolve against the Buildroot source directory.
  • Put the defconfig in the BR2_EXTERNAL tree’s configs/ directory, and use O= so the source directory holds neither your files nor your build output.

Where this leaves your build

Three things are now true that were not true at the end of Part 1: the image carries the packages you chose deliberately, the Buildroot source directory is unmodified upstream code, and every file you own sits in a separate tree you can put under version control on its own. The build writes into neither of those directories.

The practical result is that upgrading Buildroot becomes unpacking a new release and pointing it at the same tree, instead of resolving merge conflicts in code you did not write. Record the release you tested against in that tree, keep shipping images from clean builds, and the layout will hold as the board grows a kernel configuration, an overlay and a patch directory.

Was this worth your time?

Frequently asked questions

Do I have to pass BR2_EXTERNAL on every make command?
No. Buildroot saves the value in a hidden .br2-external.mk file in the output directory, so later commands in that same output directory pick it up. You do need to pass it again for a new or cleaned output directory, and you can change it at any time by passing a new value.

Which three files must a BR2_EXTERNAL tree contain?
external.desc, external.mk and Config.in. The last two may be empty. external.desc must contain a name field using only the characters [A-Za-z0-9_], and may contain an optional one-line desc field.

Why can I not find a package in menuconfig even though Buildroot has it?
Its depends on conditions are not satisfied by your current configuration, so the option is hidden rather than shown as unavailable. For example htop declares depends on !BR2_STATIC_LIBS, so it disappears from the menu on a static-only toolchain.

Can I remove a package by deselecting it and rebuilding?
No. Buildroot does not record which package installed which files into the target and staging directories, so removing a package is not supported without rebuilding from scratch. The files stay in the image.

Is a relative BR2_EXTERNAL or O= path relative to my current directory?
No. Both are interpreted relative to the main Buildroot source directory, not to the current working directory and not to the output directory. Use absolute paths if there is any doubt.

Further reading

RB
Raghu Bharadwaj

Founder, TECH VEDA — 20+ years teaching the Linux kernel, device drivers and embedded systems.

Follow on LinkedIn

Get new posts by email

Kernel, embedded Linux and AI-era engineering — a few sharp reads a month. No spam.

We email occasionally and never share your address.