Martín's blog

What we build together

How to integrate systemd-sysupdate with your Yocto-based image

The Yocto project has well-established OS update mechanisms available via third-party layers. But, did you know that recent releases of Yocto already come with a simple update mechanism?

The goal of this blog post is to present an alternative that doesn’t require a third-party layer and explain how it can be integrated with your Yocto-based image.

systemd-sysupdate #

Enter systemd-sysupdate: a mechanism capable of automatically discovering, downloading, and installing A/B-style OS updates. In a nutshell, it provides:

Together with automatic boot assessment, systemd-boot, and other tools, we can turn this OS update mechanism into a comprehensive alternative for common scenarios.

Yocto integration #

sysupdate has been available with Yocto releases for a few years now but, in order use it, it requires a few steps:

  1. Identifying the OS resources that need to be updated.
  2. Versioning these resources and the OS.
  3. Enabling sysupdate and providing transfer files for each resource.
  4. Serving updates via a web server.

OS resources to update #

The resources that need to be updated will depend on how the distribution is set up. For this post we’re assuming the following:

A Yocto-based image like this can be described as follows:

kas-poky-demo.yml:

INIT_MANAGER = "systemd"
EFI_PROVIDER = "systemd-boot"
INITRAMFS_IMAGE = "core-image-minimal-initramfs"
QB_KERNEL_ROOT = ""
QB_DEFAULT_KERNEL = "none"
IMAGE_FSTYPES = "wic"
WKS_FILE = "core-image-demo.wks.in"

recipes-core/images/core-image-demo.bb:

SUMMARY = "A demo image with UKI support enabled"
LICENSE = "MIT"
UKI_CMDLINE = "rootwait root=PARTLABEL=rootfs console=${KERNEL_CONSOLE}"
inherit core-image uki

wic/core-image-demo.wks.in:

part /boot --ondisk sda --fstype vfat --part-name ESP --part-type c12a7328-f81f-11d2-ba4b-00a0c93ec93b --source bootimg-efi --sourceparams="loader=systemd-boot,install-kernel-into-boot-dir=false" --align 1024 --active --fixed-size 100M
part / --ondisk sda --fstype=ext4 --source rootfs --part-name rootfs --part-type 4f68bce3-e8cd-4db1-96e7-fbcaf984b709 --align 1024 --use-uuid --fixed-size 300M
bootloader --ptable gpt --timeout=5

Under this specific setup, a full OS update would consist of the following resources:

As mentioned before, updating files and partitions is supported by sysupdate. So, we’re good.

Versioning resources and the OS #

In order for sysupdate to determine the current version of the OS, it looks for the os-release file and inspects it for an IMAGE_VERSION field. Therefore, the image version must be included.

Resources that require updating must also be versioned with the image version. Following our previous assumptions:

To implement these changes in your Yocto-based image, the following recipes should be added or overridden:

recipes-core/os-release/os-release.bbappend:

OS_RELEASE_FIELDS += " \
IMAGE_VERSION \
"

OS_RELEASE_UNQUOTED_FIELDS += " \
IMAGE_VERSION \
"

Note that the value of IMAGE_VERSION can be hardcoded, provided by the continuous integration pipeline or determined at build-time (e.g., the current date and time).

recipes-core/images/core-image-demo.bb:

-UKI_CMDLINE = "rootwait root=PARTLABEL=rootfs console=${KERNEL_CONSOLE}"
+UKI_FILENAME = "uki_${IMAGE_VERSION}.efi"
+UKI_CMDLINE = "rootwait root=PARTLABEL=rootfs_${IMAGE_VERSION} console=${KERNEL_CONSOLE}"

wic/core-image-demo.wks.in:

-part / --ondisk sda --fstype=ext4 --source rootfs --part-name rootfs --part-type 4f68bce3-e8cd-4db1-96e7-fbcaf984b709 --align 1024 --use-uuid --fixed-size 300M
+part / --ondisk sda --fstype=ext4 --source rootfs --part-name "rootfs_${IMAGE_VERSION}" --part-type 4f68bce3-e8cd-4db1-96e7-fbcaf984b709 --align 1024 --use-uuid --fixed-size 300M

In the above recipes, we’re adding the suffix to the UKI filename and partition name, and we’re also coupling our UKI directly to its correspondent rootfs partition.

Enabling systemd-sysupdate #

By default, sysupdate is disabled in Yocto’s systemd recipe and there are no “default” transfer files for sysupdate. Therefore you must:

  1. Override systemd build configuration options and dependencies.
  2. Write transfer files for each resource that needs to be updated.
  3. Extend the partitions kickstart file with an additional partition that must mirror the original rootfs partition. This is to support an A/B OS update scheme.

To implement these changes in your Yocto-based image, the following recipes should be added or modified:

recipes-core/systemd/systemd_%.bbappend:

EXTRA_OEMESON:append = " \
-Dfdisk=enabled \
-Dsysupdate=enabled \
-Dsysupdated=enabled \
"

SRC_URI += " \
file://60-rootfs.transfer \
file://70-kernel.transfer \
"

do_install:append() {
install -d ${D}${base_libdir}/sysupdate.d
install -m 0644 ${UNPACKDIR}/60-rootfs.transfer ${D}${base_libdir}/sysupdate.d/
install -m 0644 ${UNPACKDIR}/70-kernel.transfer ${D}${base_libdir}/sysupdate.d/
}

Note that some minor details are omitted from this snippet, but you can find the full source files down below.

recipes-core/systemd/systemd/60-rootfs.transfer:

[Transfer]
ProtectVersion=%A
Verify=no

[Source]
Type=url-file
Path=http://10.0.2.2:3333/
MatchPattern=rootfs_@v.ext4

[Target]
Type=partition
Path=auto
MatchPattern=rootfs_@v
MatchPartitionType=root
InstancesMax=2

recipes-core/systemd/systemd/70-kernel.transfer:

[Transfer]
ProtectVersion=%A
Verify=no

[Source]
Type=url-file
Path=http://10.0.2.2:3333/
MatchPattern=uki_@v.efi

[Target]
Type=regular-file
Path=/EFI/Linux
PathRelativeTo=boot
MatchPattern=uki_@v+@l-@d.efi uki_@v+@l.efi uki_@v.efi
Mode=0444
TriesLeft=3
TriesDone=0
InstancesMax=2

These transfer files define what exactly constitutes a full OS update. Each file contains the following sections:

For more information about these section properties check the sysupdate.d documentation.

wic/core-image-demo.wks.in::

 part / --ondisk sda --fstype=ext4 --source rootfs --part-name "rootfs_${IMAGE_VERSION}" --part-type 4f68bce3-e8cd-4db1-96e7-fbcaf984b709 --align 1024 --use-uuid --fixed-size 300M
+part --ondisk sda --source empty --part-name "_empty" --part-type 4f68bce3-e8cd-4db1-96e7-fbcaf984b709 --align 1024 --use-uuid --fixed-size 300M

Note that the _empty partition name is sysupdate’s naming convention for the partition resource type.

Serving the updates #

Updates can be served locally via regular directories or remotely via a regular HTTP/HTTPS web server. For Over-the-air (OTA) updates, HTTP/HTTPS is the correct option. Any web server can be used.

ls -1 ./server/
rootfs_0.ext4
rootfs_1.ext4
SHA256SUMS
uki_0.efi
uki_1.efi

When using HTTP/HTTPS, sysupdate will request a SHA256SUMS checksum file. This file acts as the update server’s “manifest”, describing what updated resources are available.

sha256sum * > SHA256SUMS
python3 -m http.server 3333

Demo #

If you’re interested in seeing these steps in action, watch our presentation at Embedded Recipes 2025 from last May.

Recording from Embedded Recipes 2025

Demo source files #

The source files of the demo shown here and in the presentation are available on GitHub. Give it a try!