Package Structure
{:.gc-basic}
Basic
Every Buildroot package lives in package/<name>/ and consists of at minimum two files:
package/
└── myapp/
├── Config.in ← Kconfig entry (menuconfig visibility)
└── myapp.mk ← Build instructions
Larger packages may add:
package/myapp/
├── Config.in
├── myapp.mk
├── 0001-fix-cross-compile.patch ← patches applied during build
├── 0002-add-hardening-flags.patch
└── myapp.hash ← SHA256 checksum of the downloaded tarball
Core Makefile Variables
| Variable | Purpose | Example |
|---|---|---|
MYAPP_VERSION |
Version string (used in tarball name) | 1.4.2 |
MYAPP_SITE |
Download URL (without filename) | https://example.com/releases |
MYAPP_SOURCE |
Tarball filename | myapp-$(MYAPP_VERSION).tar.gz |
MYAPP_LICENSE |
SPDX license identifier | GPL-2.0-or-later |
MYAPP_LICENSE_FILES |
Path to license file inside tarball | COPYING |
MYAPP_DEPENDENCIES |
Buildroot packages this depends on | libcurl openssl |
Package Infrastructure Macros
Buildroot provides ready-made infrastructure for common build systems:
| Macro | Use when | Example packages |
|---|---|---|
generic-package |
Custom Makefile, no autotools | BusyBox, kernel modules |
autotools-package |
./configure && make && make install |
most GNU utilities |
cmake-package |
CMake-based projects | many modern C++ libraries |
python-package |
Python setup.py / pyproject.toml |
Python applications |
perl-package |
CPAN modules | Perl apps |
meson-package |
Meson build system | GLib, GStreamer |
golang-package |
Go modules | Prometheus node exporter |
Writing a .mk File
{:.gc-basic}
Basic
Minimal Generic Package (custom C app, local Makefile)
# package/myapp/myapp.mk
MYAPP_VERSION = 1.2.0
MYAPP_SITE = https://releases.mycompany.com/myapp
MYAPP_SOURCE = myapp-$(MYAPP_VERSION).tar.gz
MYAPP_LICENSE = MIT
MYAPP_LICENSE_FILES = LICENSE
MYAPP_DEPENDENCIES = libcurl
define MYAPP_BUILD_CMDS
$(MAKE) $(TARGET_CONFIGURE_OPTS) -C $(@D) all
endef
define MYAPP_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/myapp $(TARGET_DIR)/usr/bin/myapp
$(INSTALL) -D -m 0644 $(@D)/myapp.conf.example \
$(TARGET_DIR)/etc/myapp/myapp.conf
endef
$(eval $(generic-package))
Key variables available inside .mk files:
$(TARGET_CC) # cross-compiler: aarch64-buildroot-linux-gnu-gcc
$(TARGET_CFLAGS) # target CFLAGS (march, mtune, optimization)
$(TARGET_LDFLAGS) # target LDFLAGS
$(TARGET_CONFIGURE_OPTS) # CC, CXX, LD, CFLAGS, LDFLAGS all at once
$(HOST_DIR) # output/host/ — for host tool installs
$(STAGING_DIR) # output/staging/ — headers/libs for cross-compile
$(TARGET_DIR) # output/target/ — target rootfs
$(@D) # package build directory: output/build/myapp-1.2.0/
Autotools Package Example
# package/myservice/myservice.mk
MYSERVICE_VERSION = 2.0.1
MYSERVICE_SITE = https://github.com/myorg/myservice/releases/download/v$(MYSERVICE_VERSION)
MYSERVICE_SOURCE = myservice-$(MYSERVICE_VERSION).tar.xz
MYSERVICE_LICENSE = Apache-2.0
MYSERVICE_LICENSE_FILES = LICENSE.txt
MYSERVICE_DEPENDENCIES = host-pkgconf openssl libsystemd
MYSERVICE_CONF_OPTS = \
--enable-daemon \
--disable-tests \
--with-ssl=$(STAGING_DIR)/usr \
--sysconfdir=/etc
# Install systemd unit file manually (not handled by make install)
define MYSERVICE_INSTALL_INIT_SYSTEMD
$(INSTALL) -D -m 0644 $(MYSERVICE_PKGDIR)/myservice.service \
$(TARGET_DIR)/usr/lib/systemd/system/myservice.service
endef
$(eval $(autotools-package))
CMake Package Example
# package/libfoo/libfoo.mk
LIBFOO_VERSION = 3.1.0
LIBFOO_SITE = https://github.com/example/libfoo/archive/refs/tags
LIBFOO_SOURCE = v$(LIBFOO_VERSION).tar.gz
LIBFOO_LICENSE = BSD-2-Clause
LIBFOO_LICENSE_FILES = COPYING
LIBFOO_INSTALL_STAGING = YES # install headers/libs to staging/
LIBFOO_CONF_OPTS = \
-DBUILD_SHARED_LIBS=ON \
-DBUILD_TESTS=OFF \
-DENABLE_EXAMPLES=OFF
$(eval $(cmake-package))
Git Source with a Specific Commit
MYAPP_VERSION = a3f92c1b8d45e7f0123456789abcdef012345678
MYAPP_SITE = https://github.com/myorg/myapp.git
MYAPP_SITE_METHOD = git
MYAPP_GIT_SUBMODULES = YES # optional: clone submodules too
Local Source Tree (for development)
# Point to a local directory instead of downloading:
# In .config or via make menuconfig → Build options → location of a custom file
# Or set BR2_OVERRIDE_SRCDIR in local.mk:
echo "MYAPP_OVERRIDE_SRCDIR = /home/dev/myapp-src" >> local.mk
# Now make myapp will rsync from /home/dev/myapp-src instead of downloading
# Make changes locally and run:
make myapp-rebuild
Config.in Entry
{:.gc-mid}
Intermediate
Basic Config.in
# package/myapp/Config.in
config BR2_PACKAGE_MYAPP
bool "myapp"
depends on BR2_PACKAGE_LIBCURL
depends on BR2_TOOLCHAIN_HAS_THREADS
select BR2_PACKAGE_OPENSSL
help
myapp is a lightweight daemon for collecting sensor data
and forwarding it to a cloud backend over HTTPS.
https://github.com/myorg/myapp
Adding to the Package Menu
Edit package/Config.in to include your new entry under the appropriate category:
# In package/Config.in, find the relevant menu and add:
menu "My Company Packages"
source "package/myapp/Config.in"
source "package/myservice/Config.in"
endmenu
Conditional Options
config BR2_PACKAGE_MYAPP
bool "myapp"
depends on BR2_PACKAGE_LIBCURL
# Only available on ARM or x86-64
depends on BR2_arm || BR2_aarch64 || BR2_x86_64
# Not compatible with uClibc (uses getaddrinfo_a)
depends on !BR2_TOOLCHAIN_USES_UCLIBC
help
...
config BR2_PACKAGE_MYAPP_ENABLE_TLS
bool "enable TLS support"
depends on BR2_PACKAGE_MYAPP
select BR2_PACKAGE_OPENSSL
default y
help
Enable TLS/HTTPS support in myapp using OpenSSL.
config BR2_PACKAGE_MYAPP_BACKEND_URL
string "default backend URL"
depends on BR2_PACKAGE_MYAPP
default "https://api.mycompany.com/v2"
help
The default backend URL compiled into myapp. Can be
overridden at runtime via /etc/myapp/myapp.conf.
Virtual Packages
For packages that can be provided by multiple implementations (e.g., a JSON library):
# In package/libjson-provider/Config.in:
config BR2_PACKAGE_HAS_LIBJSON
bool
config BR2_PACKAGE_PROVIDES_LIBJSON
string
default "json-c" if BR2_PACKAGE_JSON_C
default "jansson" if BR2_PACKAGE_JANSSON
Board Directory
{:.gc-mid}
Intermediate
Board-specific files (scripts, configs, image layout) live in board/<vendor>/<board>/:
board/
└── mycompany/
└── myboard/
├── linux.config ← kernel defconfig fragment
├── uboot-fragment.config ← U-Boot config fragment
├── genimage.cfg ← flash image layout
├── post-build.sh ← runs after target/ is populated
├── post-image.sh ← runs after images are created
└── rootfs-overlay/ ← files merged onto target/
└── etc/
└── myapp/
└── myapp.conf
genimage.cfg — Flash Image Layout
# board/mycompany/myboard/genimage.cfg
image boot.vfat {
vfat {
files = {
"bcm2711-rpi-4-b.dtb",
"start4.elf",
"fixup4.dat",
"config.txt",
"u-boot.bin"
}
}
size = 32M
}
image rootfs.ext4 {
ext4 {
label = "rootfs"
}
mountpoint = "/"
size = 512M
}
image sdcard.img {
hdimage {
partition-table-type = "mbr"
}
partition boot {
partition-type = 0xC
bootable = true
image = "boot.vfat"
offset = 8M
size = 32M
}
partition rootfs {
partition-type = 0x83
image = "rootfs.ext4"
}
}
Reference in your defconfig:
BR2_ROOTFS_POST_IMAGE_SCRIPT="board/mycompany/myboard/post-image.sh"
BR2_ROOTFS_POST_IMAGE_SCRIPT_ARGS="board/mycompany/myboard/genimage.cfg"
post-build.sh — Customise target/ Before Packaging
#!/bin/bash
# board/mycompany/myboard/post-build.sh
# $1 = path to target directory (output/target/)
# Called AFTER all packages are installed, BEFORE filesystem images are created
set -e
TARGET_DIR="$1"
# Set a custom hostname
echo "myboard-prod" > "${TARGET_DIR}/etc/hostname"
# Enable a service by creating the symlink systemd expects
mkdir -p "${TARGET_DIR}/etc/systemd/system/multi-user.target.wants"
ln -sf /usr/lib/systemd/system/myservice.service \
"${TARGET_DIR}/etc/systemd/system/multi-user.target.wants/myservice.service"
# Remove development files not needed at runtime
rm -rf "${TARGET_DIR}/usr/include"
rm -rf "${TARGET_DIR}/usr/share/man"
# Write a build timestamp
date -u +"%Y-%m-%dT%H:%M:%SZ" > "${TARGET_DIR}/etc/build-timestamp"
echo "post-build.sh: target customisation complete"
post-image.sh — Generate the Final SD Card Image
#!/bin/bash
# board/mycompany/myboard/post-image.sh
# $1 = path to images directory (output/images/)
# $2 = genimage config path (passed via BR2_ROOTFS_POST_IMAGE_SCRIPT_ARGS)
set -e
BOARD_DIR="$(dirname "$0")"
IMAGES_DIR="$1"
GENIMAGE_CFG="${2:-${BOARD_DIR}/genimage.cfg}"
GENIMAGE_TMP="${BUILD_DIR}/genimage.tmp"
# Clean temporary directory
rm -rf "${GENIMAGE_TMP}"
# Run genimage to assemble the final SD card image
genimage \
--rootpath "${TARGET_DIR}" \
--tmppath "${GENIMAGE_TMP}" \
--inputpath "${IMAGES_DIR}" \
--outputpath "${IMAGES_DIR}" \
--config "${GENIMAGE_CFG}"
echo "SD card image: ${IMAGES_DIR}/sdcard.img"
Filesystem Overlay
{:.gc-adv}
Advanced
A rootfs overlay is a directory whose contents are copied verbatim onto output/target/ after all packages are installed. It is the cleanest way to ship configuration files, startup scripts, and device-specific data without creating a Buildroot package.
Configuring the Overlay Path
# In menuconfig:
# System configuration → Root filesystem overlay directories
# Enter: board/mycompany/myboard/rootfs-overlay
# .config equivalent:
BR2_ROOTFS_OVERLAY="board/mycompany/myboard/rootfs-overlay"
# Multiple overlays (space-separated, applied left to right):
BR2_ROOTFS_OVERLAY="board/common/rootfs-overlay board/mycompany/myboard/rootfs-overlay"
Example Overlay Tree
board/mycompany/myboard/rootfs-overlay/
├── etc/
│ ├── network/
│ │ └── interfaces ← static IP configuration
│ ├── myapp/
│ │ └── myapp.conf ← application configuration
│ ├── ssh/
│ │ └── sshd_config ← hardened SSH config
│ └── fstab ← custom mount table
├── usr/
│ └── share/
│ └── myapp/
│ └── default.json ← app data file
└── etc/init.d/
└── S99myapp ← SysVinit start script
Network Interface Config Example
# board/mycompany/myboard/rootfs-overlay/etc/network/interfaces
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address 192.168.1.100
netmask 255.255.255.0
gateway 192.168.1.1
dns-nameservers 8.8.8.8
File Permissions in Overlays
By default, overlay files are installed with whatever permissions they have in the overlay directory. For special permissions (setuid, sticky) use a users table:
# BR2_ROOTFS_USERS_TABLES = board/mycompany/myboard/users.table
# Format: username uid group gid password home shell groups comment
myappd 1001 myappd 1001 = /var/lib/myapp /bin/false - - My app daemon user
For file ownership, use post-build.sh or set permissions in the package’s INSTALL_TARGET_CMDS:
# In post-build.sh — fix permissions on sensitive config
chmod 0600 "${TARGET_DIR}/etc/myapp/myapp.conf"
chown 1001:1001 "${TARGET_DIR}/etc/myapp/myapp.conf" 2>/dev/null || true
Saving and Sharing Config
{:.gc-adv}
Advanced
Saving a Defconfig
# Save only the non-default values (sparse, human-readable)
make savedefconfig BR2_DEFCONFIG=board/mycompany/myboard/myboard_defconfig
# The resulting file is clean and diff-friendly:
cat board/mycompany/myboard/myboard_defconfig
# BR2_aarch64=y
# BR2_cortex_a72=y
# BR2_TOOLCHAIN_EXTERNAL=y
# BR2_TOOLCHAIN_EXTERNAL_BOOTLIN=y
# BR2_TOOLCHAIN_EXTERNAL_BOOTLIN_AARCH64_GLIBC_STABLE=y
# BR2_LINUX_KERNEL=y
# BR2_LINUX_KERNEL_DEFCONFIG="bcm2711"
# BR2_PACKAGE_DROPBEAR=y
# BR2_PACKAGE_MYAPP=y
# BR2_TARGET_ROOTFS_EXT2=y
# BR2_TARGET_ROOTFS_EXT2_4=y
BR2_EXTERNAL — Out-of-Tree Packages
BR2_EXTERNAL lets you keep your company-specific packages, configs, and board files completely separate from the Buildroot source tree. This is the correct approach for products:
my-br-external/
├── Config.in ← top-level Kconfig for your packages
├── external.desc ← name and description of the external tree
├── external.mk ← top-level Makefile inclusion
├── board/
│ └── mycompany/
│ └── myboard/ ← board files (same structure as above)
├── configs/
│ └── myboard_defconfig ← your board defconfigs
└── package/
├── Config.in ← includes all package Config.in files
├── myapp/
│ ├── Config.in
│ └── myapp.mk
└── myservice/
├── Config.in
└── myservice.mk
external.desc:
name: MYCOMPANY
desc: My Company Buildroot external tree
external.mk:
include $(sort $(wildcard $(BR2_EXTERNAL_MYCOMPANY_PATH)/package/*/*.mk))
Using it:
# Single external tree
make BR2_EXTERNAL=/path/to/my-br-external myboard_defconfig
make -j$(nproc)
# Multiple external trees (colon-separated)
make BR2_EXTERNAL=/path/to/my-bsp:/path/to/my-apps myboard_defconfig
The BR2_EXTERNAL path is stored in output/.br2-external.mk so subsequent make calls don’t need to re-specify it.
Interview Q&A
{:.gc-iq}
Interview Q&A
Q1 — Basic: What is the difference between generic-package and autotools-package?
generic-packageprovides no predefined build steps — you must define<PKG>_BUILD_CMDSand<PKG>_INSTALL_TARGET_CMDSyourself. Use it for software with a hand-written Makefile or a non-standard build system.autotools-packageautomatically calls./configure,make, andmake installwith the correct cross-compilation flags (--host,--build,--prefix). It also handlesautoreconfif needed. Useautotools-packagewhenever the upstream project uses GNU Autotools — you only need to specify<PKG>_CONF_OPTSfor any non-default configure arguments.
Q2 — Basic: What is $1 in a post-build.sh script?
$1is the path to the target directory —output/target/— the root filesystem tree that will be packaged into images. Buildroot callspost-build.sh "$TARGET_DIR"after all packages are installed and beforepost-image.shruns and filesystem images are created. Everything you write into$1will end up in the final image. You must not rely on$0to find the script’s own directory; use theBOARD_DIRenvironment variable that Buildroot exports, or compute it with$(dirname "$(realpath "$0")").
Q3 — Intermediate: When would you use a rootfs overlay versus a package for deploying a configuration file?
Use a rootfs overlay for files that are purely device-specific configuration with no build step — network interface configs, SSH authorized keys, application config files, and custom
/etc/hostname. The overlay is simple, version-controlled alongside board files, and requires no Makefile knowledge. Use a package when the file needs to be generated from a template (e.g., a config file with values substituted at build time), when the file has install-time logic, when the file belongs to a software component that also ships binaries, or when you need fine-grained dependency tracking (only install this file if package X is enabled).
Q4 — Intermediate: How do you add an init/startup script for a custom daemon?
For BusyBox SysVinit (the default): place an
S??myservicescript inboard/.../rootfs-overlay/etc/init.d/. Buildroot’s init runs all scripts in/etc/init.d/in lexical order. For systemd: place a.serviceunit file inrootfs-overlay/usr/lib/systemd/system/and create thewantssymlink inpost-build.sh, or use theMYSERVICE_INSTALL_INIT_SYSTEMDhook in your.mkfile which is called automatically whenBR2_INIT_SYSTEMD=y. In the.mkfile,inherit systemdis not a thing in Buildroot — you write an explicitdefine MYSERVICE_INSTALL_INIT_SYSTEMDblock containing$(INSTALL)commands.
Q5 — Advanced: What is the purpose of BR2_EXTERNAL and how does it differ from just putting files in the Buildroot tree?
BR2_EXTERNALallows your company-specific files to live in a completely separate repository from Buildroot. This means you can update Buildroot (e.g., pull a new stable branch for security fixes) without touching your product code, and vice versa. WithoutBR2_EXTERNAL, your files are mixed into the Buildroot tree, making upstream merges messy and your proprietary code potentially visible in version control alongside an open-source project.BR2_EXTERNALtrees are also composable — you can stack multiple external trees (BSP layer + application layer) using colon-separated paths.
Q6 — Advanced: How do you debug a package that fails to build?
First, check the build log at
output/build/<pkg>-<ver>/<pkg>.log— Buildroot stores the full output of each build step there. Runmake <pkg> V=1to see every command executed with full flags. If configure fails, inspectoutput/build/<pkg>-<ver>/config.logfor the failing test. For cross-compilation errors, check that$(TARGET_CC),$(STAGING_DIR), and$(TARGET_CFLAGS)are being picked up correctly — look for accidental use of the host compiler. If the package uses CMake, runmake <pkg>-rebuildwithBR2_ENABLE_RUNTIME_DEBUG=yto get verbose CMake output. For interactive debugging,make <pkg>-configurethencd output/build/<pkg>-<ver>/ && $(pwd)/../../host/bin/<cross>-gcc ...to manually test compilation.
References
{:.gc-ref}
References
| Resource | Link |
|---|---|
| Buildroot Manual — Adding Packages | buildroot.org/downloads/manual/manual.html#adding-packages |
| Buildroot Manual — Board Support | buildroot.org/downloads/manual/manual.html#board-support |
| Buildroot Manual — BR2_EXTERNAL | buildroot.org/downloads/manual/manual.html#outside-br-custom |
| genimage Documentation | github.com/pengutronix/genimage |
| Bootlin Buildroot Training Slides | bootlin.com/doc/training/buildroot/buildroot-labs.pdf |
| Embedded Linux Wiki — Buildroot | elinux.org/Buildroot |