Mender’s integration into Yocto typically relies on dynamic image generation through classes like mender-image and mender-part-images. These classes automatically create the required partition layout and generate both disk images and OTA artifacts. While convenient, this abstraction can become limiting when you need precise control over your partition layout, want to understand exactly what’s happening during image generation, or are porting to hardware with unusual boot requirements.
This tutorial demonstrates an alternative approach: using explicit WKS (Wic Kickstart) files to define the Mender partition layout directly. The reference implementation targets Raspberry Pi 4 but the concepts apply to any platform.
Prerequisites
This tutorial assumes you’re comfortable with:
- Yocto layer structure and recipe syntax
- Basic WIC concepts and
.wksfile format - KAS build configurations
- Mender A/B update concepts
Bootloader Integration
Mender’s A/B update mechanism requires bootloader cooperation. After the Mender client writes a new rootfs to the inactive partition, the bootloader must:
- Switch to booting from the newly written partition
- Track boot attempts and detect failures
- Roll back to the previous partition if the new system fails to boot
Mender supports several bootloader integrations out of the box: U-Boot, GRUB, and UEFI. Custom integrations are possible for other bootloaders. This tutorial uses U-Boot as the example since it’s common on embedded ARM platforms like the Raspberry Pi, but the WKS partitioning concepts apply regardless of which bootloader you use.
The bootloader integration determines how boot state is stored and how the WKS file must accommodate it. With U-Boot, this means a raw environment block before the partition table (see manual U-Boot integration for details). With GRUB, it might be an EFI system partition with grubenv. Your WKS file must match your chosen bootloader’s requirements.
The Mender Partition Layout
A Mender-enabled system requires several distinct storage areas. Note that “partition” here refers to the logical concept of reserved storage - the actual implementation varies by platform. Some areas may be true partitions, others may be raw regions at fixed offsets, and some may share space within a larger partition.
-
Bootloader and boot state storage - Where the bootloader binary, configuration, and A/B switching state reside. This is highly platform-specific: it might be a FAT32 boot partition, an EFI system partition, raw storage at fixed offsets, or a combination. The key requirement is that the bootloader can determine which root partition to boot and track update state.
-
Root filesystem A - The active root. This is where the system boots from initially.
-
Root filesystem B - The inactive root. Mender writes new rootfs images here during updates, then the bootloader switches to boot from this partition.
-
Data partition - Persistent across updates. Holds Mender state, device identity, and application data that should survive updates.
The critical constraint: Root A and Root B must be exactly the same size. Mender uses raw block copying for updates, so size mismatches cause failures.
The WKS File
The OpenEmbedded Kickstart (.wks) format defines partition layouts for WIC image generation. Here’s the complete WKS file for Raspberry Pi 4 (mender-explicit-raspberrypi4-64.wks):
# U-Boot environment - raw storage for A/B boot state (not a partition)
# Other bootloaders handle this differently; this is Raspberry Pi + U-Boot specific
part --source rawcopy --sourceparams="file=uboot.env" --ondisk mmcblk0 --align 8192 --no-table
# Boot storage (100MB, FAT32) - kernel, device tree, U-Boot binary
# This is a true partition on RPi; other platforms may differ
part /boot --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --active --align 8192 --size 100
# Root filesystem A (512MB, ext4, active)
# Standard rootfs source - this is the active root partition for booting
part / --source rootfs --ondisk mmcblk0 --fstype=ext4 --label rootA --align 8192 --fixed-size 512M
# Root filesystem B (512MB, ext4, inactive)
# Empty partition with same size as Root A - used for A/B updates
# Mender client will write new rootfs here during updates
part --ondisk mmcblk0 --fstype=ext4 --label rootB --align 8192 --fixed-size 512M
# Data partition (128MB, ext4, persistent)
# Uses rootfs source with change-directory=data to populate with /data contents
part /data --source rootfs --change-directory=data --ondisk mmcblk0 --fstype=ext4 --label data --align 8192 --size 128
# Use MBR partition table (compatible with Raspberry Pi bootloader)
bootloader --ptable msdos
Let’s examine the key elements:
Bootloader state (U-Boot specific): The --no-table flag places this data at a raw offset without a partition table entry. The --source rawcopy with file=uboot.env copies the U-Boot environment file directly. This file contains the variables that decide whether to boot from Root A or Root B, along with retry counters for rollback logic. This is one way to handle boot state - other bootloaders store it differently. GRUB uses grubenv within a partition, some platforms use dedicated EEPROM, and others embed state in the bootloader binary itself.
Boot partition: In this Raspberry Pi example, a dedicated FAT32 partition holds the kernel, device tree, and U-Boot binary. The --source bootimg-partition source is a standard WIC plugin that collects files from IMAGE_BOOT_FILES and combines them with boot-related files from the rootfs. The --active flag marks this partition as bootable in the MBR. On other platforms, boot files might live in an EFI system partition, be loaded directly from raw flash, or reside elsewhere entirely - adapt according to your hardware’s boot flow.
Root partitions: Both use --fixed-size 512M to ensure identical sizes. Root A uses --source rootfs to populate it with the filesystem contents. Root B has no source - it’s created empty. During an OTA update, Mender writes the new rootfs here.
Data partition: The --change-directory=data option is the key trick here. Instead of copying the entire rootfs, WIC only copies contents from the /data directory within the rootfs. This mirrors what mender-dataimg.bbclass does.
Partition table: Raspberry Pi requires MBR format (--ptable msdos). Other platforms may need GPT.
The Dual-Root Variant
For development and testing, a second WKS file populates both root partitions:
# Root filesystem B (512MB, ext4, inactive)
# Also populated with rootfs content - same as Root A for immediate A/B testing
part --source rootfs --ondisk mmcblk0 --fstype=ext4 --label rootB --align 8192 --fixed-size 512M
The only difference is adding --source rootfs to the Root B partition. This creates a larger image but allows immediate A/B switching tests without performing an OTA update first.
The .bbclass
The mender-explicit-wic.bbclass handles the integration between Mender and WIC:
# Inherit essential Mender functionality but NOT image generation classes
# Note: mender-uboot is specific to U-Boot integration; use mender-grub for GRUB-based systems
inherit mender-setup mender-systemd mender-uboot
# Enable only essential Mender features (no dynamic image generation)
# The mender-uboot feature is bootloader-specific; substitute mender-grub for GRUB systems
MENDER_FEATURES_ENABLE:append = " \
mender-uboot \
mender-auth-install \
mender-update-install \
mender-systemd \
mender-growfs-data \
"
# Explicitly disable dynamic partitioning features
MENDER_FEATURES_DISABLE:append = " mender-image mender-part-images"
This is the critical part: we inherit the Mender setup classes for configuration variables and bootloader integration, but explicitly disable mender-image and mender-part-images. These are the classes that would normally generate the partition layout dynamically.
The bootloader class (mender-uboot in this example) handles the specifics of A/B boot switching. For GRUB-based systems, you would inherit mender-grub instead and enable the mender-grub feature. The WKS file would also need adjustment to match the bootloader’s expectations.
The WKS file selection uses a machine-specific pattern:
WKS_FILE = "mender-explicit-${MACHINE}.wks"
WKS_SEARCH_PATH:prepend = "${LAYERDIR_meta-mender-explicit-wic}/wic:"
This allows adding support for additional machines by creating appropriately named WKS files.
Artifact Generation
Since we disabled mender-image, we need our own .mender artifact generation:
IMAGE_FSTYPES:append = " wic wic.bz2 mender"
IMAGE_CMD:mender() {
if [ -z "${MENDER_ARTIFACT_NAME}" ]; then
bbfatal "MENDER_ARTIFACT_NAME not set."
fi
ROOTFS_IMG="${IMGDEPLOYDIR}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.${ARTIFACTIMG_FSTYPE}"
MENDER_ARTIFACT="${IMGDEPLOYDIR}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.mender"
DEVICE_TYPE_ARGS=""
for DEVICE_TYPE in ${MENDER_DEVICE_TYPES_COMPATIBLE}; do
DEVICE_TYPE_ARGS="${DEVICE_TYPE_ARGS} -t ${DEVICE_TYPE}"
done
mender-artifact write rootfs-image \
--artifact-name ${MENDER_ARTIFACT_NAME} \
${DEVICE_TYPE_ARGS} \
--file ${ROOTFS_IMG} \
--output-path ${MENDER_ARTIFACT}
}
do_image_mender[depends] += " \
mender-artifact-native:do_populate_sysroot \
${PN}:do_image_${ARTIFACTIMG_FSTYPE} \
"
This custom IMAGE_CMD:mender function takes the ext4 rootfs image and wraps it into a .mender artifact using mender-artifact. The artifact can then be deployed via Mender server for OTA updates.
The Image Recipe
The image recipe ties everything together:
SUMMARY = "Mender A/B demo image using explicit WIC partitioning"
inherit core-image mender-explicit-wic
IMAGE_INSTALL = "\
packagegroup-core-boot \
${CORE_IMAGE_EXTRA_INSTALL} \
mender-auth \
mender-update \
mender-configure \
mender-connect \
kernel-image \
kernel-devicetree \
"
ROOTFS_POSTPROCESS_COMMAND += "create_mender_data_structure; "
create_mender_data_structure() {
install -d ${IMAGE_ROOTFS}/data
install -d ${IMAGE_ROOTFS}/data/mender
install -d ${IMAGE_ROOTFS}/data/mender-configure
}
The create_mender_data_structure function creates the /data directory structure in the rootfs. WIC then picks up these contents via the --change-directory=data directive in the WKS file to populate the data partition.
KAS Configuration
KAS is a build tool that simplifies Yocto project setup. The configuration file raspberrypi4-64-mender-explicit-wic.yml assembles all required layers:
header:
version: 14
machine: raspberrypi4-64
distro: poky
repos:
poky:
url: git://git.yoctoproject.org/poky
commit: c162696dae5798e2ab1198403d0bc1d65d64068d
layers:
meta:
meta-poky:
meta-yocto-bsp:
meta-openembedded:
url: git://git.openembedded.org/meta-openembedded
commit: e92d0173a80ea7592c866618ef5293203c50544c
layers:
meta-oe:
meta-mender:
url: https://github.com/mendersoftware/meta-mender.git
commit: 0d393c2267a92091f975eb932338c07109f5f5fd
layers:
meta-mender-core:
meta-raspberrypi:
url: https://github.com/agherzan/meta-raspberrypi.git
commit: 1091bde25e9ebaea33114edb85e4aee931d105f3
meta-mender-community:
layers:
meta-mender-raspberrypi:
kas/demos/meta-mender-explicit-wic:
local_conf_header:
mender-artifact: |
MENDER_ARTIFACT_NAME = "explicit-wic"
IMAGE_FSTYPES:prepend = "ext4 "
raspberrypi: |
RPI_USE_U_BOOT = "1"
ENABLE_UART = "1"
MENDER_BOOT_PART_SIZE_MB = "100"
IMAGE_FSTYPES:remove = " rpi-sdimg"
mender-explicit-demo: |
MENDER_FEATURES_ENABLE:append = " \
mender-uboot \
mender-auth-install \
mender-update-install \
mender-systemd \
mender-growfs-data \
"
MENDER_FEATURES_DISABLE:append = " mender-image mender-part-images"
PREFERRED_VERSION_mender-artifact-native = "4.%"
target:
- mender-explicit-demo-image
The meta-mender-explicit-wic layer is included as a sublayer of meta-mender-community. The configuration explicitly disables Mender’s dynamic image features and sets up the Raspberry Pi for U-Boot booting. The mender-uboot feature and RPI_USE_U_BOOT settings are specific to this U-Boot-based example; other platforms would use their appropriate bootloader integration.
Customization
Porting to Other Machines
To support a different machine:
-
Create a new WKS file named
mender-explicit-${MACHINE}.wksin thewic/directory -
Adjust the
--ondiskparameter:mmcblk0for SD cards and eMMCsdafor SATA/USB drivesnvme0n1for NVMe drives
-
Choose the partition table type:
--ptable msdosfor legacy BIOS/MBR systems--ptable gptfor UEFI systems
-
Adjust the boot partition source if needed:
--source bootimg-partitionworks for most cases- EFI systems may need
--source bootimg-efi
-
Match the bootloader integration:
- For U-Boot with environment in raw storage: include a
--no-tableblock as shown in this example - For U-Boot with environment in a partition: adjust accordingly, or omit if stored in FAT boot partition
- For GRUB: use an EFI system partition and inherit
mender-grubinstead ofmender-uboot - For custom bootloaders: implement appropriate state storage and modify the bbclass accordingly
- Some platforms may not need a separate boot partition at all if the bootloader loads directly from root
- For U-Boot with environment in raw storage: include a
Adjusting Partition Sizes
Root partition sizes are controlled by --fixed-size. Both Root A and Root B must use the same value:
part / --source rootfs --ondisk mmcblk0 --fstype=ext4 --label rootA --align 8192 --fixed-size 1024M
part --ondisk mmcblk0 --fstype=ext4 --label rootB --align 8192 --fixed-size 1024M
The data partition uses --size (minimum size) rather than --fixed-size, allowing it to expand with mender-growfs-data on first boot:
part /data --source rootfs --change-directory=data --ondisk mmcblk0 --fstype=ext4 --label data --align 8192 --size 256
Adding Custom Partitions
You can add additional partitions. Insert them after the data partition to avoid breaking Mender’s expectations:
# Recovery partition
part /recovery --source rawcopy --sourceparams="file=recovery.img" --ondisk mmcblk0 --fstype=ext4 --label recovery --align 8192 --size 256
Ensure any custom partitions don’t interfere with Mender’s partition numbering expectations. Mender typically expects:
- Partition 1: Boot
- Partition 2: Root A
- Partition 3: Root B
- Partition 4: Data
Additional partitions should come after these.
Building
Standard build:
kas build kas/demos/raspberrypi4-64-mender-explicit-wic.yml
To use the dual-root variant for testing:
WKS_FILE=mender-explicit-raspberrypi4-64-dual-root.wks kas build kas/demos/raspberrypi4-64-mender-explicit-wic.yml
Build Outputs
After a successful build, find these files in tmp/deploy/images/raspberrypi4-64/:
mender-explicit-demo-image-raspberrypi4-64.wic- Full disk image for SD card flashingmender-explicit-demo-image-raspberrypi4-64.wic.bz2- Compressed disk imagemender-explicit-demo-image-raspberrypi4-64.mender- OTA artifact for Mender server deployment
Flash the WIC image to an SD card:
bzcat mender-explicit-demo-image-raspberrypi4-64.wic.bz2 | sudo dd of=/dev/sdX bs=4M status=progress
Verification
After booting, verify Mender is functional:
mender show-artifact
This should display the artifact name set in the configuration (explicit-wic).
To verify the partition layout:
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT
You should see the expected partition structure with labels boot, rootA, rootB, and data.
Summary
This approach gives you complete visibility and control over the Mender partition layout while maintaining full A/B update compatibility. The WKS file serves as explicit documentation of your disk layout, making it easier to understand, modify, and debug compared to the dynamic generation approach.
The trade-off is additional maintenance: you’re now responsible for keeping the WKS file, bbclass, and image recipe in sync with Mender’s requirements. For most production deployments, the standard Mender integration is simpler. But when you need that extra control, explicit WKS partitioning delivers it.
Resources
Demo Source Code
- meta-mender-explicit-wic layer - Complete demo implementation
- KAS configuration file - Build configuration
Mender Documentation
Yocto Documentation
Repositories
- meta-mender - Core Mender Yocto layer
- meta-mender-community - Community board integrations
- KAS - Yocto build setup tool