Distro Configuration
{:.gc-basic}
Basic
A Yocto distribution is the policy layer — it defines the global defaults that apply to every package and image in the build. While the machine configuration answers “what hardware?”, the distro configuration answers “what kind of Linux?”: which C library, which init system, which default compiler flags, which optional features are compiled in.
Creating a Distro Config File
mkdir -p meta-myproduct/conf/distro
cat > meta-myproduct/conf/distro/mydistro.conf << 'EOF'
# meta-myproduct/conf/distro/mydistro.conf
DISTRO = "mydistro"
DISTRO_NAME = "My Company Embedded Linux"
DISTRO_VERSION = "2.1.0"
DISTRO_CODENAME = "alpine"
MAINTAINER = "embedded@mycompany.com"
# Inherit Poky's defaults, then override what we need
require conf/distro/poky.conf
EOF
Activate it in build/conf/local.conf:
DISTRO = "mydistro"
DISTRO_FEATURES
DISTRO_FEATURES is a space-separated list of capability flags that recipes inspect to decide what to compile. Setting or removing a feature here affects every package in the build.
# Start from Poky defaults, then customise:
DISTRO_FEATURES = "acl argp ext2 ipv4 ipv6 largefile usbgadget \
usbhost wifi bluetooth nfs zeroconf pci \
systemd polkit"
# Remove features we don't need (cleaner than redefining the whole list):
DISTRO_FEATURES:remove = "wayland x11 opengl vulkan"
DISTRO_FEATURES:remove = "pcmcia nfs" # no NFS on our product
DISTRO_FEATURES:append = " ima" # add IMA integrity measurement
| Feature | Effect on recipes |
|---|---|
systemd |
Enables systemd unit installation across all packages |
wayland |
Builds Wayland compositor support in GTK, Qt, Mesa |
x11 |
Builds X11 support in nearly everything — large |
wifi |
Enables wpa_supplicant, wireless tools |
bluetooth |
Enables BlueZ stack and BT support in PulseAudio |
polkit |
Enables PolicyKit integration in D-Bus services |
usrmerge |
Merges /bin, /sbin, /lib into /usr (modern distros) |
seccomp |
Compiles seccomp support into systemd, containers |
Init System
# Use systemd as the init system (replaces sysvinit/BusyBox init)
DISTRO_FEATURES:append = " systemd"
VIRTUAL-RUNTIME_init_manager = "systemd"
VIRTUAL-RUNTIME_initscripts = "systemd-compat-units"
# Or use SysVinit via BusyBox (smaller, simpler):
VIRTUAL-RUNTIME_init_manager = "sysvinit"
VIRTUAL-RUNTIME_initscripts = "initscripts"
C Library Selection
# glibc (default in Poky) — best compatibility
TCLIBC = "glibc"
# musl — smaller, stricter, good for security-focused products
TCLIBC = "musl"
# Note: changing TCLIBC requires a complete rebuild from scratch
Compiler and Security Flags
# Add hardening flags to every compiled package
SECURITY_CFLAGS = "-fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2"
SECURITY_LDFLAGS = "-Wl,-z,relro,-z,now"
SECURITY_PIC_CFLAGS = "-fPIE"
SECURITY_PIC_LDFLAGS = "-pie"
# Enable address space layout randomisation everywhere
GCCPIE = "--enable-default-pie"
# Strip debug symbols from target binaries (reduces size)
INHIBIT_PACKAGE_STRIP = ""
# Or keep them for field debugging:
# INHIBIT_PACKAGE_STRIP = "1"
Image Recipes
{:.gc-mid}
Intermediate
An image recipe is a special BitBake recipe that describes what goes into a root filesystem image. It produces the files in tmp/deploy/images/<machine>/.
Standard Reference Images
| Image | Description | Approximate size |
|---|---|---|
core-image-minimal |
BusyBox + kernel, no ssh, ~10 MB | Smallest possible |
core-image-base |
Minimal + package management support | ~20 MB |
core-image-full-cmdline |
Full command-line tools | ~50 MB |
core-image-sato |
Full GNOME/Sato GUI | ~200 MB+ |
core-image-weston |
Wayland/Weston compositor | ~150 MB+ |
Writing a Custom Image Recipe
# meta-myproduct/recipes-core/images/myproduct-image.bb
SUMMARY = "My Company production image"
DESCRIPTION = "Minimal image for MyBoard sensor gateway product"
# Inherit the core-image class (provides do_rootfs, IMAGE_INSTALL machinery)
inherit core-image
# Inherit features for read-only rootfs, package group support
IMAGE_FEATURES += "read-only-rootfs ssh-server-openssh"
# Base packages (in addition to IMAGE_FEATURES)
IMAGE_INSTALL = " \
packagegroup-core-boot \
packagegroup-myproduct-base \
myapp \
myservice \
htop \
strace \
tcpdump \
curl \
"
# Filesystem types to generate
IMAGE_FSTYPES = "ext4 squashfs wic"
# Maximum rootfs size in KB (512 MB)
IMAGE_ROOTFS_SIZE = "524288"
# Extra space for /var, logs, runtime data (in KB)
IMAGE_ROOTFS_EXTRA_SPACE = "65536"
# License manifest and SBOM
LICENSE_FLAGS_ACCEPTED = "commercial"
IMAGE_FEATURES Reference
# Security / access
IMAGE_FEATURES += "ssh-server-openssh" # OpenSSH sshd
IMAGE_FEATURES += "ssh-server-dropbear" # Dropbear (smaller)
IMAGE_FEATURES += "allow-root-login" # root login via SSH
IMAGE_FEATURES += "allow-empty-password" # dev only!
# Development / debug
IMAGE_FEATURES += "debug-tweaks" # root no-password + tools
IMAGE_FEATURES += "tools-debug" # gdb, strace, ltrace
IMAGE_FEATURES += "tools-profile" # perf, oprofile
IMAGE_FEATURES += "tools-sdk" # compiler on target
IMAGE_FEATURES += "dev-pkgs" # -dev packages in image
# Filesystem
IMAGE_FEATURES += "read-only-rootfs" # mount rootfs read-only
IMAGE_FEATURES += "read-only-rootfs-delayed-postinsts" # run postinsts on first boot
# Package management
IMAGE_FEATURES += "package-management" # opkg/rpm/dpkg on target
COMPATIBLE_MACHINE
Restrict an image recipe to specific machines:
# Only build this image for our custom boards, not QEMU or RPi
COMPATIBLE_MACHINE = "myboard|myboard-v2|myboard-lite"
# Alternatively, exclude a machine:
# COMPATIBLE_MACHINE = "^(?!qemux86).*"
Package Groups
{:.gc-mid}
Intermediate
Package groups are lightweight recipes that aggregate other packages by function. They make image recipes readable and allow feature-based composition.
Writing a Package Group
# meta-myproduct/recipes-core/packagegroups/packagegroup-myproduct-base.bb
SUMMARY = "Base packages for all MyProduct variants"
LICENSE = "MIT"
inherit packagegroup
RDEPENDS:${PN} = " \
busybox \
dropbear \
iproute2 \
iptables \
curl \
openssl \
e2fsprogs-resize2fs \
watchdog \
tzdata \
"
# Optional: add packages only when a DISTRO_FEATURE is active
RDEPENDS:${PN} += "${@bb.utils.contains('DISTRO_FEATURES', 'wifi', 'wpa-supplicant iw', '', d)}"
RDEPENDS:${PN} += "${@bb.utils.contains('DISTRO_FEATURES', 'bluetooth', 'bluez5', '', d)}"
Splitting Package Groups by Function
# packagegroup-myproduct-connectivity.bb
inherit packagegroup
RDEPENDS:${PN} = "wpa-supplicant iw bluez5 modemmanager networkmanager"
# packagegroup-myproduct-sensors.bb
inherit packagegroup
RDEPENDS:${PN} = "i2c-tools spidev libgpiod"
# packagegroup-myproduct-ota.bb
inherit packagegroup
RDEPENDS:${PN} = "mender-client mender-auth swupdate"
# packagegroup-myproduct-debug.bb (dev/staging images only)
inherit packagegroup
RDEPENDS:${PN} = "htop strace tcpdump gdb gdbserver ldd"
Using Package Groups in Image Recipes
# myproduct-image.bb
IMAGE_INSTALL = " \
packagegroup-core-boot \
packagegroup-myproduct-base \
packagegroup-myproduct-connectivity \
packagegroup-myproduct-sensors \
myapp \
"
# myproduct-dev-image.bb extends production image
require myproduct-image.bb
IMAGE_INSTALL += "packagegroup-myproduct-debug"
IMAGE_FEATURES += "debug-tweaks tools-debug"
FEATURE_PACKAGES — Image-Feature-Triggered Groups
# In your distro or image recipe, map IMAGE_FEATURES to package groups:
FEATURE_PACKAGES_myproduct-sensors = "packagegroup-myproduct-sensors"
# Now adding "myproduct-sensors" to IMAGE_FEATURES automatically installs the group:
IMAGE_FEATURES += "myproduct-sensors"
WIC — Disk Image Tool
{:.gc-adv}
Advanced
WIC (Wic Is Crappy, self-deprecating name) is the Yocto tool for assembling bootable disk images. It reads a .wks (Wic KickStart) file that describes the partition layout and fills in the actual image content.
Writing a .wks File
# meta-myproduct/wic/myboard-sdcard.wks
# Boot partition: FAT32, holds kernel + DTB + bootloader config
part /boot --source bootimg-partition --fstype=vfat \
--label boot --active --align 4 --size 64
# Root filesystem: ext4
part / --source rootfs --fstype=ext4 \
--label rootfs --align 4 --size 512
# Data partition: persists across OTA updates
part /data --fstype=ext4 \
--label data --align 4 --size 256
# Bootloader (raw, before first partition)
bootloader --ptable mbt \
--append "root=/dev/mmcblk0p2 rootfstype=ext4 rootwait console=ttyS0,115200"
GPT Layout for Production Boards
# meta-myproduct/wic/myboard-gpt.wks
# A/B partition layout for OTA updates
# ESP partition for GRUB/systemd-boot
part /boot --source bootimg-efi --sourceparams="loader=grub" \
--fstype=vfat --label boot --active --align 1024 --size 64
# Rootfs A (active)
part / --source rootfs --fstype=ext4 \
--label rootfs-a --align 1024 --size 1024
# Rootfs B (inactive — OTA writes here)
part --fstype=ext4 --label rootfs-b --align 1024 --size 1024
# Persistent data (survives rootfs OTA update)
part /data --fstype=ext4 --label data --align 1024 --size 512
bootloader --ptable gpt
Building and Inspecting WIC Images
# Build the WIC image as part of the normal image build
bitbake myproduct-image
# → tmp/deploy/images/myboard/myproduct-image-myboard.wic
# Or create a WIC image from already-built artifacts:
wic create myboard-sdcard.wks \
-e myproduct-image \
--outdir output/
# List partitions in a WIC image
wic ls myproduct-image-myboard.wic
# List files in partition 1
wic ls myproduct-image-myboard.wic:1
# Copy a file into partition 1 (without reflashing)
wic cp myapp-update myproduct-image-myboard.wic:1/
# Flash to SD card
sudo dd if=myproduct-image-myboard.wic of=/dev/sdX bs=4M status=progress
SDK and eSDK Generation
{:.gc-adv}
Advanced
The Yocto SDK gives application developers a self-contained cross-compilation environment without needing to run a full Yocto build.
Standard SDK
# Generate the SDK installer from a built image
bitbake myproduct-image -c populate_sdk
# Output: tmp/deploy/sdk/
# mydistro-glibc-x86_64-myproduct-image-cortexa53-myboard-toolchain-2.1.0.sh
# Install the SDK (on any Linux x86-64 machine)
./mydistro-glibc-x86_64-myproduct-image-cortexa53-myboard-toolchain-2.1.0.sh
# Installs to /opt/mydistro/2.1.0/ by default
# Using the SDK:
source /opt/mydistro/2.1.0/environment-setup-cortexa53-poky-linux
echo $CC
# aarch64-poky-linux-gcc --sysroot=/opt/mydistro/2.1.0/sysroots/cortexa53-poky-linux
# Cross-compile a project:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$OECORE_NATIVE_SYSROOT/usr/share/cmake/OEToolchainConfig.cmake ..
cmake --build build
Controlling SDK Contents
# Add extra target packages to the SDK sysroot (for linking against):
TOOLCHAIN_TARGET_TASK += "libcurl-dev openssl-dev zlib-dev"
# Add extra host tools to the SDK:
TOOLCHAIN_HOST_TASK += "nativesdk-cmake nativesdk-pkgconfig"
# In your distro.conf or image recipe:
SDK_NAME = "${DISTRO}-${TCLIBC}-${SDK_ARCH}-${IMAGE_BASENAME}-${TUNE_PKGARCH}"
SDK_VERSION = "${DISTRO_VERSION}"
eSDK (Extensible SDK)
The eSDK includes devtool, BitBake, and the layer stack — application developers can add recipes and rebuild images without a full Yocto checkout.
# Generate eSDK:
bitbake myproduct-image -c populate_sdk_ext
# Install eSDK:
./mydistro-glibc-x86_64-myproduct-image-cortexa53-myboard-toolchain-ext-2.1.0.sh
# Initialize eSDK environment:
source /opt/mydistro/2.1.0/environment-setup-cortexa53-poky-linux
# devtool workflow inside eSDK:
devtool add myapp https://github.com/myorg/myapp.git # create recipe
devtool build myapp # cross-compile
devtool deploy-target myapp root@192.168.1.100 # deploy over SSH
devtool finish myapp /path/to/meta-myproduct/ # save recipe to layer
eSDK vs Standard SDK Comparison
| Aspect | Standard SDK | eSDK |
|---|---|---|
| Size | ~300–600 MB | ~1–3 GB |
| Build new recipes | No | Yes (devtool) |
| Update SDK packages | No | Yes (devtool sdk-update) |
| Deploy to target | Manual | devtool deploy-target |
| Suitable for | CMake/Makefile devs | Full embedded devs |
| Setup time | Minutes | 10–20 minutes |
OTA Update Strategy
{:.gc-adv}
Advanced
Production embedded Linux products must support reliable Over-The-Air (OTA) updates. The Yocto ecosystem integrates well with several OTA frameworks.
Read-Only Root Filesystem
The foundation of any robust OTA strategy is a read-only rootfs. If the rootfs cannot be written, a failed update cannot corrupt it:
# In your image recipe:
IMAGE_FEATURES += "read-only-rootfs"
# Applications write to /var (tmpfs or data partition), not /etc:
# /etc → read-only (baked into rootfs)
# /var → read-write (tmpfs or separate data partition)
# /data → read-write (persistent data partition, survives OTA)
overlayfs for /etc
When applications need to modify /etc at runtime (e.g., generating SSH host keys), use overlayfs:
# In your image recipe or distro config:
IMAGE_FEATURES += "overlayfs-etc"
# This mounts /etc as an overlayfs:
# lower: /etc (read-only from rootfs)
# upper: /var/volatile/etc (read-write tmpfs)
# merged: /etc (appears writable to applications)
A/B Partition OTA with Mender
# meta-mender/recipes-mender/ provides integration
# In your local.conf or image recipe:
INHERIT += "mender-full"
MENDER_ARTIFACT_NAME = "myproduct-v${DISTRO_VERSION}"
# Partition layout (wks file):
# mmcblk0p1: boot (FAT32)
# mmcblk0p2: rootfs-a (ext4) ← active
# mmcblk0p3: rootfs-b (ext4) ← inactive (OTA writes here)
# mmcblk0p4: data (ext4) ← persistent
# Mender client variables:
MENDER_STORAGE_DEVICE = "/dev/mmcblk0"
MENDER_BOOT_PART = "${MENDER_STORAGE_DEVICE}p1"
MENDER_ROOTFS_PART_A = "${MENDER_STORAGE_DEVICE}p2"
MENDER_ROOTFS_PART_B = "${MENDER_STORAGE_DEVICE}p3"
MENDER_DATA_PART = "${MENDER_STORAGE_DEVICE}p4"
A/B Update with RAUC
# Add RAUC to your image:
IMAGE_INSTALL += "rauc"
# RAUC system configuration (deployed to /etc/rauc/system.conf):
[system]
compatible=myboard
bootloader=uboot
[keyring]
path=/etc/rauc/keyring.pem
[slot.rootfs.0]
device=/dev/mmcblk0p2
type=ext4
bootname=rootfs-a
[slot.rootfs.1]
device=/dev/mmcblk0p3
type=ext4
bootname=rootfs-b
[slot.boot.0]
device=/dev/mmcblk0p1
type=vfat
swupdate — Single Image Update
# swupdate reads a .swu archive (CPIO containing sw-description + images)
# sw-description:
software =
{
version = "2.1.0";
myboard = (
{
type = "rawfile";
filename = "rootfs.ext4";
device = "/dev/mmcblk0p3"; # write to inactive partition
sha256 = "...";
},
{
type = "shellscript";
filename = "post-install.sh";
}
);
}
# Trigger an update:
swupdate -i myproduct-v2.1.0.swu -l /tmp/swupdate.log
Hawkbit — Update Management Server
# Mender and swupdate both support Hawkbit for update management:
IMAGE_INSTALL += "swupdate swupdate-hawkbit-handler"
# Configure hawkbit client in /etc/swupdate.cfg:
[suricatta]
tenant = "default"
id = "myboard-serial-001"
url = "https://hawkbit.mycompany.com"
polldelay = 300 # check every 5 minutes
Interview Q&A
{:.gc-iq}
Interview Q&A
Q1 — Basic: What is the difference between DISTRO_FEATURES and IMAGE_FEATURES?
DISTRO_FEATUREScontrols what is compiled into packages — it affects how recipes build their binaries. For example,DISTRO_FEATURES:remove = "x11"causes every package that optionally uses X11 (GTK, Mesa, Qt, etc.) to compile without X11 support. It is a distribution-wide policy that applies to all images built from that distro.IMAGE_FEATUREScontrols what packages and configurations are installed in a specific image’s rootfs. For example,IMAGE_FEATURES += "ssh-server-openssh"adds the OpenSSH daemon to the image but does not affect how any package was compiled. You can have multiple images with differentIMAGE_FEATURESbuilt from the sameDISTRO_FEATURESpolicy.
Q2 — Intermediate: What is a WIC kickstart file and why is it needed?
A
.wksfile (WIC KickStart) is a partition layout description that tells thewictool how to assemble a bootable disk image. It specifies partition types (FAT32 for boot, ext4 for rootfs), sizes, labels, alignment, bootloader placement, and which kernel/rootfs images to fill each partition with. It is necessary because embedded boards have non-uniform boot requirements — a Raspberry Pi expects a FAT32 first partition with specific files, an i.MX board expects U-Boot at a raw byte offset, and an x86 board may use GPT with a UEFI System Partition. Without a.wksfile you would have to manually runparted,mkfs,dd, andmount/cpfor every build.
Q3 — Intermediate: What is the difference between the standard SDK and the eSDK?
The standard SDK is a self-contained cross-toolchain installer (~300–600 MB) that gives application developers the cross-compiler, sysroot headers, and target libraries needed to cross-compile against your specific image. It is ideal for developers who use CMake, Makefile, or Autotools and just need to compile an application for the target. The eSDK (extensible SDK) includes all of that plus BitBake, the full layer stack, and
devtool(~1–3 GB). eSDK users can add new recipes to the SDK, rebuild the rootfs, and usedevtool deploy-targetto push binaries to a live target over SSH. Use the standard SDK for pure application developers; use the eSDK for platform/BSP developers who extend the system software.
Q4 — Advanced: How do you make the root filesystem read-only in Yocto, and what challenges does this create?
Add
read-only-rootfstoIMAGE_FEATURES. Yocto will setROOTFS_READ_ONLY = "1", which causes the image creation scripts to run package post-installation scripts at build time (not at first boot), and to add aromount option for the rootfs in the image. Challenges: (1) Applications that write to/etcat runtime (SSH key generation, NTP configs, DHCP leases) must be redirected to/var— useIMAGE_FEATURES += "overlayfs-etc"or symlinks. (2)/varshould be a tmpfs (ephemeral) or a separate writable data partition (persistent). (3) Package managers cannot update packages at runtime — all updates come through OTA full-image replacement. (4) Log rotation must write to/var/lograther than/etc.
Q5 — Advanced: What is COMPATIBLE_MACHINE and when would you use it?
COMPATIBLE_MACHINEis a regular expression that restricts which machines a recipe or image can be built for. IfMACHINEdoes not match the pattern, BitBake skips that recipe entirely. Use it when: (1) an image contains hardware-specific packages (GPU firmware, proprietary drivers) that do not exist for other machines; (2) a BSP recipe (U-Boot config, device tree) is board-specific; (3) you want to prevent accidentally building a Raspberry Pi image for an x86 machine. Example:COMPATIBLE_MACHINE = "myboard|myboard-v2"ensures the recipe is only built when targeting those two boards. For kernel recipes,COMPATIBLE_MACHINEis almost always set to the specific board or SoC family.
Q6 — Advanced: How do package groups simplify image recipe maintenance in a multi-product portfolio?
Instead of listing 40 individual packages in every image recipe, you define package groups by function —
packagegroup-myproduct-base,packagegroup-myproduct-connectivity,packagegroup-myproduct-sensors. Image recipes then install these groups. When you add a new dependency (e.g., a second wifi chip driver), you update thepackagegroup-myproduct-connectivityrecipe once and all images that include it automatically pick up the change. For a multi-product portfolio (sensor gateway, industrial controller, HMI device), each product image installs a common base group plus its product-specific group. The maintenance burden scales with the number of package groups, not with the number of images times the number of packages.
References
{:.gc-ref}
References
| Resource | Link |
|---|---|
| Yocto Project Mega-Manual | docs.yoctoproject.org/singleindex.html |
| Yocto Project Images Reference | docs.yoctoproject.org/ref-manual/images.html |
| Yocto Wic Tool Documentation | docs.yoctoproject.org/dev-manual/wic.html |
| Yocto SDK Manual | docs.yoctoproject.org/sdk-manual/ |
| Mender OTA Documentation | docs.mender.io |
| RAUC Update Framework | rauc.readthedocs.io |
| swupdate Documentation | sbabic.github.io/swupdate |
| Eclipse Hawkbit (OTA server) | eclipse.dev/hawkbit |