all
Stage 10

Buildroot Custom Packages

Add custom software to Buildroot — write .mk package files, Config.in entries, use board defconfigs and overlay directories, add post-build and post-image scripts, and create a custom BSP.

10 min read
52139 chars

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-package provides no predefined build steps — you must define <PKG>_BUILD_CMDS and <PKG>_INSTALL_TARGET_CMDS yourself. Use it for software with a hand-written Makefile or a non-standard build system. autotools-package automatically calls ./configure, make, and make install with the correct cross-compilation flags (--host, --build, --prefix). It also handles autoreconf if needed. Use autotools-package whenever the upstream project uses GNU Autotools — you only need to specify <PKG>_CONF_OPTS for any non-default configure arguments.

Q2 — Basic: What is $1 in a post-build.sh script?

$1 is the path to the target directory — output/target/ — the root filesystem tree that will be packaged into images. Buildroot calls post-build.sh "$TARGET_DIR" after all packages are installed and before post-image.sh runs and filesystem images are created. Everything you write into $1 will end up in the final image. You must not rely on $0 to find the script’s own directory; use the BOARD_DIR environment 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??myservice script in board/.../rootfs-overlay/etc/init.d/. Buildroot’s init runs all scripts in /etc/init.d/ in lexical order. For systemd: place a .service unit file in rootfs-overlay/usr/lib/systemd/system/ and create the wants symlink in post-build.sh, or use the MYSERVICE_INSTALL_INIT_SYSTEMD hook in your .mk file which is called automatically when BR2_INIT_SYSTEMD=y. In the .mk file, inherit systemd is not a thing in Buildroot — you write an explicit define MYSERVICE_INSTALL_INIT_SYSTEMD block 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_EXTERNAL allows 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. Without BR2_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_EXTERNAL trees 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. Run make <pkg> V=1 to see every command executed with full flags. If configure fails, inspect output/build/<pkg>-<ver>/config.log for 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, run make <pkg>-rebuild with BR2_ENABLE_RUNTIME_DEBUG=y to get verbose CMake output. For interactive debugging, make <pkg>-configure then cd 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