The First Steps With Buildroot

For the past 10 years, I have used only one tool to create Linux firmware images: Yocto. And that is a good tool. As far as I know, there aren’t too many alternatives, but there is one option worth considering that I’ve heard about multiple times: Buildroot. I’ve wanted to give it a try for a long time, and after all these years the time has finally come. This text contains instructions on how to get started with Buildroot and some first impressions of it from a long-time Yocto developer.

One day I’ll have a router running on OpenWrt and a basic understanding of what ELBE is.

What Is Buildroot

Buildroot is a tool for building root file systems, kernels, bootloaders, firmware images and toolchains. It aims for simplicity and efficiency. The first sentence sounds a lot like Yocto, the second one not so much. But yes, Buildroot attempts to solve more or less the same, very complex problem. However, Buildroot’s approach feels more like “start quickly from the minimal system and expand from there”, whereas Yocto’s reference distro Poky is more a general-purpose tool that requires “some” pruning. Well, technically you’re not supposed to use Poky but build your distro from scratch instead, but I don’t think that ever happens.

As a developer, you define the target hardware you want to build for, configure the Linux kernel, select packages to install, etc. Configuration is done using a kernel-like menuconfig, and the packages are defined with Makefile syntax, so there aren’t too many new things to learn for experienced developers. There are also plenty of supported packages, so usually there is no need to start writing package definitions for the dependencies, and you can focus on your code instead. All in all, sounds good!

Building with Buildroot

Let’s get started then. Buildroot has good documentation for the first build. To begin, you need to install the dependencies listed here, it’s all fairly basic stuff. Alternatively, you can use a provided Vagrant image with the dependencies pre-installed, but I didn’t try it. My goal was to build a root file system and a kernel for an aarch64 QEMU machine. To do that I can simply run the following:

# Download and extract buildroot
wget http://buildroot.org/downloads/buildroot-2024.11.1.tar.gz
tar xzvf buildroot-2024.11.1.tar.gz
cd buildroot-2024.11.1
# Generate default config
make qemu_aarch64_virt_defconfig
# Build all
make

Additional configuration to the build can be performed with make menuconfig if required. These commands generate all the build artifacts to the ./output/images directory. The build process also creates a nifty script that can be used to easily launch QEMU with the just-built files:

./output/images/start-qemu.sh

The simple minimal build process is quite straightforward, as is using QEMU with the artefacts. However, this was a lot faster process than with Yocto, more on that later.

Adding a Custom Package

Usually, when building the firmware we are not interested in running Linux on an embedded device. We are more interested in running our code on an embedded device, we just need Linux to do that. Therefore it’s useful to check how to add your own packages to Buildroot. I didn’t want to try the most trivial Hello World example. Instead, I wanted to build a package that had local source files, and the package was defined outside the Buildroot source. Also, I chose to test a dependency, so the example application depends on libcurl. Plenty of stuff for the first attempt.

Should be easy, right?

You can find the example external layer from my GitHub. In the next chapters, I’ll go through how the layer was written. It is perhaps a bit confusing, but in the end there will be simple commands that should explain it all better than my thousand words ever will.

Writing the Out-of-Tree Package

The first task is defining the folder that will be an external source directory, analogous to meta-layer in Yocto terms. The directory needs to contain three files: external.desc, external.mk and Config.in. The first file defines the name (and an optional description) of the external directory. external.mk should include the .mk files of the external packages, more on those files a bit later. Config.in needs to source the configuration files of the external packages, again, more on those files a bit later. Lots of manual work, but most of these files can be one- or two-liners.

Then, we create package directory, and inside that, we’ll create a directory for our package. We need to create two files in that directory: another Config.in and <package_name>.mk. The configuration file needs to contain one configuration item for selecting the package. That item should also select the dependencies to ensure they get selected. The .mk file is the harder one. It should define the package, its build and installation instructions, etc. It is the equivalent of a recipe in the Yocto world. For our example, we can utilize a recipe from this Stack Overflow answer and add the libcurl dependency to it. All good so far.

Next, we can create src directory for our local source files inside the package directory. In there, we can add the source code and the Makefile. The important part is that the make target needs to match the package name, but other than that it’s quite basic stuff.

Integrating the Package to Buildroot

After the external directory is ready, we can add it to Buildroot by calling make with BR2_EXTERNAL parameter and it should get picked up in the subsequent builds:

make BR2_EXTERNAL=./buildroot-custom-layer menuconfig

To unset the external layers, use the parameter with an empty value.

Now, in theory, we could build the package simply by calling make <package-name>. However, here things start to get a bit tricky. First of all, since our local sources are located outside the Buildroot, we need to set BR2_PACKAGE_OVERRIDE_FILE variable to define a custom override for the example package. The steps are outlined in this Stack Overflow answer, but to summarize:

  1. Create a file with the following content:
    <PACKAGE_NAME>_OVERRIDE_SRCDIR = <PATH_TO_PACKAGE_SOURCES>
  2. Set BR2_PACKAGE_OVERRIDE_FILE to point to the created file (for example by using menuconfig)

These steps define a file that allows defining non-standard source locations for packages. Buildroot assumes that it usually fetches the sources online, and if we were using for example a git repository instead of local files this wouldn’t be necessary. It feels like we’re starting to get into the hacky territory.

Then, the harder issue: libcurl dependency. If we are using the aforementioned aarch64 defconfig, nothing selects libcurl. But, defining it as a dependency for the example in the .mk file is enough to get it built. However, since libcurl is not selected in the configuration, it is not configured and doesn’t compile because SSL/TLS backend for it is not selected. This is a bit of a libcurl-specific problem, but I feel like this illustrates some of the problems of the Buildroot’s dependency and configuration system.

But yes, back to the example package. We should not configure libcurl in the package configuration, because a binary shouldn’t configure the library. Therefore, we should open make menuconfig, enable for example OpenSSL, and then edit the libcurl configuration so that the OpenSSL backend is used. I couldn’t find an automated way of doing this because as far as I know Buildroot doesn’t support configuration fragments. Echoing relevant lines to the global .config might work, but that tends to result in override warnings. These in turn result in the build working just once because the overridden variables return to their original values after one build.

After all that hassle we can build the example package with make <package-name>. If the package is enabled in .config using menuconfig it’ll get included in the image built with the plain make command. To clean the build folder you can use make <package-name>-dirclean.

Putting It All Together

I am fully aware that maybe that wasn’t the most coherent explanation, so here’s how you can get the example layer working. I have named the example application curl-example.

# Navigate to the Buildroot root
cd buildroot-2024.11.1
git clone https://github.com/ejaaskel/buildroot-custom-layer.git
make BR2_EXTERNAL=./buildroot-custom-layer menuconfig
# In the menuconfig that opens, do the following
# 1: Enable BR2_PACKAGE_OPENSSL
# 2: Enable BR2_PACKAGE_LIBCURL
# 3: Enable BR2_PACKAGE_LIBCURL_OPENSSL
# 4: Enable BR2_PACKAGE_CURL_EXAMPLE
# 5: Set BR2_PACKAGE_OVERRIDE_FILE to ./buildroot-custom-layer/buildroot_override
# Use / to search for options, then save and exit
#
# Build just the example package
make curl-example
# Clean the example package build
make curl-example-dirclean
# Build the entire image with the example package
make

The Differences to Yocto

It is impossible to write a big, comprehensive list of all the differences between Yocto and Buildroot, as I have been using Buildroot for about five hours at this point. There are a lot of similarities, as both projects aim to solve a similar problem, but a few differences are quite apparent even to a novice like me.

I’ll leave it up to you to decide which one is the evil build tool.

Configuration

“Simple” is of course subjective, but the bulk of the Buildroot configuration is done in a menuconfig, meaning that you won’t have to hunt down dozens of configuration files sprinkled around in different meta-layers. This makes the Buildroot configuration process feel quite simple. I’m not a big fan of the almost 5000 line .config Buildroot creates. But on the other hand, I’ve been going through equally long and confusing bitbake -e outputs, so I think it’s a necessary evil in these systems.

The two tools have quite different approaches to the configuration, but both are in my opinion valid methods. I like Yocto’s approach more, but I am extremely biased and cannot be trusted.

Build and Speed

The basic Buildroot build is a lot faster than the basic Yocto build. A lot. The initial Buildroot build from scratch takes about 58 minutes on my Docker setup, whereas building Yocto Scarthgap core-image-minimal with the same setup takes a whopping 225 minutes. With incremental builds the difference is smaller. Editing a simple example package and re-building an image takes about 23 seconds with Buildroot and 37 seconds with Yocto. Still, Buildroot is faster.

One thing that I like about Yocto (or actually AFAIK this is a Bitbake feature) is that it shows currently built packages and performed tasks. The log files are stored in the background. Buildroot dumps logs straight into the terminal, making the progress hard to follow. There is a print of each task when it begins, but it gets quickly lost underneath the flurry of log prints.

The basic build experience is pretty much the same for both tools. You can build both the whole image or just individual packages. Cleaning the individual packages also works for both. One thing that I noticed is that Buildroot doesn’t quite detect changes in source/configuration files as accurately as Yocto does, so you may need to manually clean up things more often.

Development

One of the strengths of Yocto is the meta-layer system that allows configuring flexible build environments. Buildroot allows similar things with external layers, but in my opinion, it feels a bit stiff and most likely doesn’t scale that well.

Writing the Buildroot package felt confusing, but I’m certain many people feel the same way about writing the first Yocto recipes. However, what I’m not fully convinced about is defining the dependencies in Buildroot. It seems to be quite manual work, and the default settings don’t always work. It is also confusing that the information seems to be sprinkled around in Config.in, .mk and the .config files. However, again I suppose something similar could be said about Yocto. Maybe libcurl wasn’t the best choice for the first example, but at least it taught me a lot about the internals of the Buildroot.

One big positive for Buildroot is that it has Chocolate Doom package. 1000 points for that. Makes it a lot easier to test if a device can run Doom or not.

Conclusion

Buildroot aims to be a simpler and more efficient tool for building Linux system images, and I think it achieves that goal well. Buildroot is better for simpler systems, where there don’t have to be multiple image variants, there are fewer supported boards or just one product that is being developed. Yocto seems to be a more complex beast, or perhaps the build systems made with Yocto just tend to grow too large.

To summarize: if you just want to run code on an embedded device, use Buildroot. If you’re building an embedded system, use Yocto. But remember, these are my opinions after a few hours of Buildroot usage, so I may be horribly wrong. In the future, I’ll continue using Yocto for my blog posts, but for quick experiments or smaller tasks, I’ll keep a Buildroot environment ready to use.

Recommended Reading

Share