<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://softwarecraft.ch//feed.xml" rel="self" type="application/atom+xml" /><link href="https://softwarecraft.ch//" rel="alternate" type="text/html" /><updated>2026-03-11T10:26:09+00:00</updated><id>https://softwarecraft.ch//feed.xml</id><title type="html">SoftwareCraft</title><subtitle>SoftwareCraft GmbH - Expert Software Delivery, CMake Expertise, C++ Coding</subtitle><entry xml:lang="en"><title type="html">Making Releases a Non-Issue: Speeding Up Delivery for Embedded Devices</title><link href="https://softwarecraft.ch//speeding-up-embedded-delivery/" rel="alternate" type="text/html" title="Making Releases a Non-Issue: Speeding Up Delivery for Embedded Devices" /><published>2026-03-11T00:00:00+00:00</published><updated>2026-03-11T00:00:00+00:00</updated><id>https://softwarecraft.ch//speeding-up-embedded-delivery</id><content type="html" xml:base="https://softwarecraft.ch//speeding-up-embedded-delivery/"><![CDATA[<p><strong>Shipping software for embedded devices has traditionally meant long release cycles, risky upgrades and “Big Bang” releases.</strong> But it doesn’t have to be that way. Being able to ship updates to your devices faster and more reliably can be a game-changer for your business. It means being able to ship earlier, respond to customer feedback more quickly, and fix bugs on the fly. When shipping software becomes a routine and safe, teams can focus on delivering value instead of managing risky releases. Moving from a few releases every odd year to a few releases every month will be a game-changer and it is not as hard as you might think.</p>

<h2 id="big-releases-are-a-big-risk">Big releases are a big risk</h2>

<p>In many embedded software projects, releases are rare and stressful events. They often involve a lot of manual work, coordination between teams, and a high risk of things going wrong. The alternative is to make releases small, frequent and boring. Instead of bundling months of development into a single firmware version, deliver incremental improvements continuously. Each change should be small enough that its impact is well understood and reversible.</p>

<p>When releases are small and routine, the feedback loop with customers becomes much shorter and product development becomes more agile. While embedded software probably never will be able to ship as fast as pure software products, there are still many ways to speed up the delivery process and make it more reliable. However, moving from multiple months between releases to a few weeks or even days can have a huge impact on your product and your customers. It allows you to respond to customer feedback more quickly, fix bugs faster, and deliver new features sooner.</p>

<h2 id="making-releases-routine">Making Releases Routine</h2>

<p>To enable faster releases for embedded devices, you need to invest in a few key capabilities that reinforce each other. Some of these are technical foundations, others are organizational habits or cultural shifts, but most can be introduced incrementally without turning the whole company upside down. The goal is to make releasing feel like just another part of everyday development work rather than a nerve‑wracking special event.</p>

<style>
    /* Default: Light theme → make box darker */
    .contrast-box {
        background-color: hsla(0, 0%, 94%, 0.918);
        margin-bottom: 1.5em;
        padding: 0.5em;
    }

    /* Dark theme → make box lighter */
    @media (prefers-color-scheme: dark) {

        .contrast-box {
            background-color: hsla(0, 0%, 89%, 0.055);
        }
    }
</style>

<div class="contrast-box">
    <h1><a href="/services/project-work/">
            Like what you read?</a></h1>

    <div style="display: grid; width: 100%; grid-template-columns: 10% 1fr; grid-gap: 1%; padding-bottom: 0.5em;">
        <div>

            <a href="/services/project-work/">
                <img src="/images/software-craft-logo_small.png" alt="SoftwareCraft Logo" style="max-width:100%;height:auto;display:block" />

            </a>

        </div>
        <div>
            We help teams design, build, and rescue complex software projects. From
            tricky architectures to hands-on delivery, we bring senior expertise where it matters most. Curious how we
            can help?
            <br />
            <br />

            <a href="/services/project-work/">Learn more</a> or
            <a href="/about/contact">or contact us</a>


        </div>

    </div>
</div>

<p>The first step is to <strong>treat releases as a routine, not a ceremony</strong>. If you ship only once or twice a year, every release feels exceptional, with custom scripts, ad‑hoc decisions and lots of manual coordination. This naturally increases risk, because nobody gets enough practice to truly master the process. By contrast, when releases happen regularly, the team gains muscle memory. You can get there by deliberately increasing the frequency of internal releases first: ship every sprint to a staging environment, then to a small set of internal devices, and only then to customers. Over time, the release pipeline becomes so familiar that pushing a new version out is as unremarkable as merging a pull request.</p>

<p>Shifting the attitude inside the team towards releases is one thing, but to go to full power you also need the technical capabilities to deliver the releases. <strong>Over‑The‑Air (OTA) updates</strong> are the technical backbone that makes this routine possible at scale. Without a solid OTA mechanism, delivering embedded software means physical access to devices, manual interventions, or complex field procedures. This makes releases cumbersome and risky.
A robust OTA solution lets you update devices in the field reliably, with support for progressive rollouts, version tracking and rollbacks. There are many mature OTA-solutions out there, and most can be conveniently integrated with your build system and CI/CD pipeline. With OTA in place, you can decouple the release process from physical logistics and manual steps. The outcome is that “shipping” becomes an automated action triggered from your pipeline rather than a logistical project involving manual coordination.</p>

<figure>
  <img src="/images/embedded-delivery/factors_for_reduced_delivery_risk.png" alt="Four factors to reduce delivery risk." onclick="toggleSize(this)" />
  <figcaption>Four factors to reduce delivery risk. (Click to enlarge, click again to reduce the size)
    
  </figcaption>
</figure>

<p>The ability to update devices in the field is a big enabler, but to profit the most from it automating the delivery pipeline is a must. Automated testing via a <strong>powerful CI/CD pipeline</strong> is what turns frequent updates from reckless to safe. If every change can be built, tested and validated automatically, then the risk of each individual release drops dramatically. The testing possibility and complexity for embedded software is often huge, but even automating a subset of tests can have a big impact on confidence. Build up an efficient and automated testing pipeline that covers the critical paths of your software. Start by putting the entire build under CI so that every commit produces a reproducible artifact. Then layer on unit tests, static analysis, and hardware‑in‑the‑loop or end‑to‑end tests where feasible. Over time, your pipeline becomes a safety net: if something slips through, the pipeline catches it before customers ever see it. This confidence is what allows you to ship more often without losing sleep.</p>

<p>Finally, shipping faster requires <strong>shifting product management away from chasing big, “heroic” releases and towards delivering features continuously</strong>. Large, monolithic releases encourage long planning cycles, heavy upfront design and a tendency to cram in “just one more feature” which delays getting real feedback. Shift from delivering major versions that “make everying better” to delivering single features or small improvements that can be released independently. Product managers play a crucial role here by setting expectations with stakeholders about incremental releases, and measuring success by customer impact rather than marketing the next big release “that makes everything better”. You can support this shift by aligning roadmaps with short delivery cadences, encouraging experimentation, and using metrics such as lead time and value delivered instead of just counting features.</p>

<p>When you combine these four elements - releases as routine, OTA as plumbing, automated testing as your safety net and product management focused on incremental value - you create a delivery system that is both faster and more resilient. Releases stop being terrifying, once‑in‑a‑while events and become a steady, predictable flow of improvements. That, in turn, lets your teams spend less time orchestrating risky upgrades and more time building products your customers actually care about.</p>]]></content><author><name>Dominik Berner</name></author><category term="embedded" /><category term="OTA" /><category term="CI/CD" /><category term="agile" /><category term="software-delivery" /><summary type="html"><![CDATA[Shipping software for embedded devices has traditionally meant long release cycles, risky upgrades and “Big Bang” releases. But it doesn’t have to be that way. Being able to ship updates to your devices faster and more reliably can be a game-changer for your business. It means being able to ship earlier, respond to customer feedback more quickly, and fix bugs on the fly. When shipping software becomes a routine and safe, teams can focus on delivering value instead of managing risky releases. Moving from a few releases every odd year to a few releases every month will be a game-changer and it is not as hard as you might think.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/embedded-delivery/thumbnail.jpg" /><media:content medium="image" url="https://softwarecraft.ch//images/embedded-delivery/thumbnail.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Encryption at Rest for a Raspberry Pi using Yocto</title><link href="https://softwarecraft.ch//encryption-at-rest/" rel="alternate" type="text/html" title="Encryption at Rest for a Raspberry Pi using Yocto" /><published>2026-01-27T00:00:00+00:00</published><updated>2026-01-27T00:00:00+00:00</updated><id>https://softwarecraft.ch//encryption-at-rest</id><content type="html" xml:base="https://softwarecraft.ch//encryption-at-rest/"><![CDATA[<p><strong>As embedded devices store more and more sensitive data, encryption-at-rest becomes a critical requirement.</strong> Especially for devices that are publicly accessible or deployed in untrusted environments, protecting data while the device is powered down is essential. Together with <a href="https://softwarecraft.ch/secure-boot-yocto/">secure boot</a>, <strong>encryption at rest</strong> ensures that data stored on the device remains confidential and tamper-proof, even if the device is physically compromised. This article describes how to implement LUKS encryption for embedded Linux devices based on the <a href="https://www.raspberrypi.com/products/compute-module-4/?variant=raspberry-pi-cm4001000">Raspberry Pi Compute Module 4</a> using the yocto Project. (Warning, this is a long read!)</p>

<script type="module">
    import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
    mermaid.initialize({ startOnLoad: true });
    await mermaid.run({
        querySelector: '.language-mermaid',
    });
</script>

<h2 id="encryption-at-rest-in-embedded-linux-in-a-nutshell">Encryption at rest in embedded Linux in a nutshell</h2>

<p>On a high level, <em>encryption at rest</em> means that all data stored on the device is encrypted when the device is powered off. This ensures that if an attacker gains physical access to the device, they cannot read the data without the encryption key. In practice, this is typically achieved by encrypting the root filesystem and any other sensitive partitions using a strong encryption algorithm such as AES. When the device boots up, the encryption key is retrieved from a secure location (e.g. One-Time Programmable memory (OTP)) and used to unlock the encrypted partitions, allowing the operating system to access the data as needed. While this adds some complexity to the boot process and comes with a slight performance overhead for I/O operations, the security benefits far outweigh these drawbacks for most applications.</p>

<p>In practice for embedded Linux devices, encryption at rest can be implemented using <a href="https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup">LUKS</a> (Linux Unified Key Setup), which is a widely used disk encryption specification for Linux. The tool to manage LUKS encrypted partitions is <a href="https://gitlab.com/cryptsetup/cryptsetup"><code class="language-plaintext highlighter-rouge">cryptsetup</code></a> and is well supported in the Linux kernel through the <a href="https://en.wikipedia.org/wiki/Dm-crypt">dm-crypt</a> kernel module.</p>

<p>Let’s look at how these steps can be implemented in detail with yocto.</p>

<h2 id="creating-a-luks-encrypted-root-filesystem-with-yocto">Creating a LUKS-encrypted root filesystem with yocto</h2>

<p>To implement encryption at rest using LUKS on embedded Linux devices based on the Raspberry Pi Compute Module 4 with the yocto Project, the following steps are necessary:</p>

<ul>
  <li>Create an initramfs image that includes the <code class="language-plaintext highlighter-rouge">cryptsetup</code> tool and any necessary dependencies.</li>
  <li>Modify the bootloader configuration to load the initramfs image during the boot process.</li>
  <li>Create an init script within the initramfs that handles the unlocking of the LUKS-encrypted root filesystem using the encryption key stored in OTP memory.</li>
  <li>Implement a provisioning process during the first boot to create the LUKS-encrypted root filesystem and store the encryption key in OTP memory.</li>
</ul>

<style>
    /* Default: Light theme → make box darker */
    .contrast-box {
        background-color: hsla(0, 0%, 94%, 0.918);
        margin-bottom: 1.5em;
        padding: 0.5em;
    }

    /* Dark theme → make box lighter */
    @media (prefers-color-scheme: dark) {

        .contrast-box {
            background-color: hsla(0, 0%, 89%, 0.055);
        }
    }
</style>

<div class="contrast-box">
    <h1><a href="/services/project-work/">
            Like what you read?</a></h1>

    <div style="display: grid; width: 100%; grid-template-columns: 10% 1fr; grid-gap: 1%; padding-bottom: 0.5em;">
        <div>

            <a href="/services/project-work/">
                <img src="/images/software-craft-logo_small.png" alt="SoftwareCraft Logo" style="max-width:100%;height:auto;display:block" />

            </a>

        </div>
        <div>
            We help teams design, build, and rescue complex software projects. From
            tricky architectures to hands-on delivery, we bring senior expertise where it matters most. Curious how we
            can help?
            <br />
            <br />

            <a href="/services/project-work/">Learn more</a> or
            <a href="/about/contact">or contact us</a>


        </div>

    </div>
</div>

<p>Since the encryption of the partitions is device specific and needs to be done only once, a provisioning process is required during the first boot of the device. As a consequence, the device image created from yocto is unencrypted initially and will only be encrypted during the provisioning process. A convenient way to achieve this is to use an A/B partition scheme, where one partition is used for provisioning and the other for normal operation. During the first boot, the init script generates a unique encryption key for the device, creates the LUKS-encrypted root filesystem, copies the unencrypted root filesystem into the encrypted container, and stores the encryption key in OTP memory. On subsequent boots, the init script retrieves the encryption key from OTP memory and unlocks the LUKS-encrypted root filesystem for normal operation. So the init script needs to handle two scenarios: the first boot (provisioning) and subsequent boots (normal operation).</p>

<p>But first of all, the initramfs image needs to be created and the bootloader configuration modified.</p>

<h3 id="create-initramfs-with-cryptsetup">Create initramfs with cryptsetup</h3>

<p>Since the initramfs is just a minimal Linux image, we can create it using yocto by defining a custom image recipe. The image recipe should include the <code class="language-plaintext highlighter-rouge">cryptsetup</code> package and any other necessary dependencies. Here is an example of how to create an initramfs image with <code class="language-plaintext highlighter-rouge">cryptsetup</code>. Let’s call this recipe <code class="language-plaintext highlighter-rouge">initramfs-cryptsetup.bb</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>inherit core-image

PACKAGE_INSTALL <span class="o">=</span> <span class="s2">"provisioning-initramfs-init cryptsetup e2fsprogs rsync userland openssl rpi-eeprom"</span>

IMAGE_FEATURES <span class="o">=</span> <span class="s2">""</span>

PACKAGE_EXCLUDE <span class="o">=</span> <span class="s2">"kernel-image-*"</span>

IMAGE_FSTYPES <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">INITRAMFS_FSTYPES</span><span class="k">}</span><span class="s2">"</span>

INITRAMFS_MAXSIZE <span class="o">=</span> <span class="s2">"262144"</span>

do_install_bootfiles<span class="o">()</span> <span class="o">{</span>
    
    <span class="nb">install</span> <span class="nt">-m</span> 644 <span class="k">${</span><span class="nv">IMGDEPLOYDIR</span><span class="k">}</span>/<span class="k">${</span><span class="nv">IMAGE_BASENAME</span><span class="k">}</span>-<span class="k">${</span><span class="nv">MACHINE</span><span class="k">}</span>.cpio.gz <span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span>/bootfiles/initramfs.cpio.gz
    
<span class="o">}</span>

addtask do_install_bootfiles after do_image_cpio before do_image_complete
</code></pre></div></div>

<p>If we look at this a bit closer, we can see that the image recipe inherits from <code class="language-plaintext highlighter-rouge">core-image</code>, which provides a basic image structure. The <code class="language-plaintext highlighter-rouge">PACKAGE_INSTALL</code> variable specifies the packages to be included in the initramfs image, including <code class="language-plaintext highlighter-rouge">cryptsetup</code> for LUKS support <code class="language-plaintext highlighter-rouge">provisioning-initramfs-init</code>, which is our custom package that contains the init script for handling the unlocking of the LUKS-encrypted root filesystem.  Additionally, other necessary packages such as <code class="language-plaintext highlighter-rouge">e2fsprogs</code>, <code class="language-plaintext highlighter-rouge">rsync</code>, <code class="language-plaintext highlighter-rouge">userland</code>, <code class="language-plaintext highlighter-rouge">openssl</code>, and <code class="language-plaintext highlighter-rouge">rpi-eeprom</code> are included to support various functionalities required for the provisioning process, create the encryption keys and store them in the OTP memory of the Raspberry Pi CM4.
Since the initramfs is intended to only run the provisioning and unlocking process, we exclude the kernel image packages by setting the <code class="language-plaintext highlighter-rouge">PACKAGE_EXCLUDE</code> variable to <code class="language-plaintext highlighter-rouge">kernel-image-*</code>. The <code class="language-plaintext highlighter-rouge">IMAGE_FSTYPES</code> variable is set to <code class="language-plaintext highlighter-rouge">${INITRAMFS_FSTYPES}</code> to specify the desired initramfs file types (e.g., cpio.gz). The <code class="language-plaintext highlighter-rouge">INITRAMFS_MAXSIZE</code> variable is set to <code class="language-plaintext highlighter-rouge">262144</code> (256MB) to define the maximum size of the initramfs image. <code class="language-plaintext highlighter-rouge">IMAGE_FEATURES</code> is deliberately left empty to avoid including unnecessary features in the initramfs image.
The <code class="language-plaintext highlighter-rouge">do_install_bootfiles</code> function installs the generated initramfs image to the appropriate location for the bootloader to access it during the boot process.</p>

<p>Next we need to make sure that this initramfs image is loaded by the bootloader during the boot process.</p>

<h3 id="modify-bootloader-configuration-to-load-initramfs">Modify bootloader configuration to load initramfs</h3>

<p>To modify the bootloader configuration to load the initramfs image during the boot process, we need to update the bootloader settings to include the initramfs image. For Raspberry Pi devices, this is typically done by modifying the <code class="language-plaintext highlighter-rouge">config.txt</code> file located in the boot partition. In the case of the Raspberry Pi recipes this can be achieved by modifying the <code class="language-plaintext highlighter-rouge">RPI_EXTRA_CONFIG</code> variable in the machine configuration file (e.g., <code class="language-plaintext highlighter-rouge">raspberrypi4.conf</code> or <code class="language-plaintext highlighter-rouge">raspberrypi-cm4.conf</code>). Here is an example of how to modify the bootloader configuration to load the initramfs image:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RPI_EXTRA_CONFIG +<span class="o">=</span> <span class="s2">"
...
initramfs initramfs.cpio.gz</span><span class="se">\n\</span><span class="s2">
..."</span>
</code></pre></div></div>

<p>This tells the bootloader to load the specified initramfs image (<code class="language-plaintext highlighter-rouge">initramfs.cpio.gz</code>) during the boot process, but it is only half the story. To make this accessible, we also need to make sure that the initramfs image is copied to the <code class="language-plaintext highlighter-rouge">boot.img</code> container during the image creation process. If your distribution is not yet using secure boot, I suggest to implement this first. Here is a <a href="https://softwarecraft.ch/secure-boot-yocto/">step by step guide for secure boot using yocto</a>.</p>

<p>In the recipe creating the <code class="language-plaintext highlighter-rouge">boot.img</code> we need to add the dependency to our initramfs image and copy it into the boot image. Here is an example of how to do this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
inherit deploy nopackages

DEPENDS +<span class="o">=</span> <span class="s2">"rpi-bootfiles rpi-cmdline rpi-config linux-raspberrypi dosfstools-native mtools-native initramfs-cryptsetup"</span>

<span class="c"># The raspberry pi bootloader files are located in the following directories</span>
BOOTFILES_DIR <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">WORKDIR</span><span class="k">}</span><span class="s2">/bootfiles"</span> 
DTBO_DIR <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span><span class="s2">"</span>

S <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">WORKDIR</span><span class="k">}</span><span class="s2">/boot-img-container"</span>

do_compile<span class="o">()</span> <span class="o">{</span>
    ...
   <span class="c"># Create boot.img file code, omitted for brevity here</span>
   ...
<span class="o">}</span>

do_compile[depends] +<span class="o">=</span> <span class="s2">"initramfs-cryptsetup:do_install_bootfiles"</span>

</code></pre></div></div>

<p>By adding the <code class="language-plaintext highlighter-rouge">initramfs-cryptsetup</code> dependency to the <code class="language-plaintext highlighter-rouge">DEPENDS</code> variable, we ensure that the initramfs image is built before the <code class="language-plaintext highlighter-rouge">boot.img</code> is created. The <code class="language-plaintext highlighter-rouge">do_compile[depends]</code> line ensures that the initramfs image is installed to the bootfiles directory before creating the <code class="language-plaintext highlighter-rouge">boot.img</code>. With these modifications and together with the modifications of the <code class="language-plaintext highlighter-rouge">RPI_EXTRA_CONFIG</code> variable, the initramfs will be included in the <code class="language-plaintext highlighter-rouge">boot.img</code> and loaded by the bootloader during the boot process. At this point we have everything in place to start looking at how the unlocking of the LUKS-encrypted root filesystem during boot and the provisioning process during the first boot can be implemented.</p>

<h3 id="init-script-for-unlocking-luks-encrypted-root-filesystem">Init script for unlocking LUKS-encrypted root filesystem</h3>

<p>Inside the initramfs there is an init script, which is a shell script that runs during the initramfs stage of the boot process. In our case, the init script is responsible for handling both the provisioning process during the first boot and the normal unlocking process during subsequent boots. First we create a new recipe for placing the init script inside the initramfs-image, let’s call it <code class="language-plaintext highlighter-rouge">provisioning-initramfs-init.bb</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SRC_URI <span class="o">=</span> <span class="s2">"file://provisioning-initramfs-init"</span>

<span class="c"># Installs shell and binaries required for the initscript</span>
RDEPENDS:<span class="k">${</span><span class="nv">PN</span><span class="k">}</span> +<span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">VIRTUAL</span><span class="p">-RUNTIME_base-utils</span><span class="k">}</span><span class="s2">"</span>

do_install<span class="o">()</span> <span class="o">{</span>
    <span class="c"># Install the init script itself</span>
    <span class="nb">install</span> <span class="nt">-m</span> 0755 <span class="k">${</span><span class="nv">WORKDIR</span><span class="k">}</span>/provisioning-initramfs-init <span class="k">${</span><span class="nv">D</span><span class="k">}</span>/init
    <span class="c"># Kernel will panic if /dev doesn't exist</span>
    <span class="nb">install</span> <span class="nt">-d</span> <span class="k">${</span><span class="nv">D</span><span class="k">}</span>/dev
    <span class="c"># Required for logging to work</span>
    <span class="nb">mknod</span> <span class="nt">-m</span> 622 <span class="k">${</span><span class="nv">D</span><span class="k">}</span>/dev/console c 5 1

    <span class="c"># Add the mount points</span>
    <span class="nb">install</span> <span class="nt">-d</span> <span class="k">${</span><span class="nv">D</span><span class="k">}</span>/mnt
<span class="o">}</span>

FILES:<span class="k">${</span><span class="nv">PN</span><span class="k">}</span> <span class="o">=</span> <span class="s2">"/init /dev/console /proc /sys /mnt"</span>
</code></pre></div></div>

<p>This recipe is included in the initramfs image by adding it to the <code class="language-plaintext highlighter-rouge">PACKAGE_INSTALL</code> variable in the <code class="language-plaintext highlighter-rouge">initramfs-cryptsetup.bb</code> recipe as shown earlier. The actual init script (<code class="language-plaintext highlighter-rouge">provisioning-initramfs-init</code>) contains the logic for both the provisioning and normal unlocking processes. Additionally, it creates the <code class="language-plaintext highlighter-rouge">/dev</code> directory and the <code class="language-plaintext highlighter-rouge">/dev/console</code> device to allow for logging over uart, but this is mainly for debug purpose.</p>

<p>So far we have set up the initramfs image with the necessary tools and created a recipe to include the init script. But this does not yet cover the actual logic for unlocking the LUKS-encrypted root filesystem and the provisioning process. Let’s look at how this can be implemented in the init script.</p>

<h4 id="the-init-script">The init script</h4>

<p>The init script is a shell script that runs during the initramfs stage of the boot process. In our case it it checks whether the device has been provisioned (i.e., whether the LUKS-encrypted root filesystem has been created and the encryption key stored in OTP memory). If not, it performs the provisioning process; otherwise, it retrieves the encryption key from OTP memory and unlocks the LUKS-encrypted root filesystem for normal operation.</p>

<p>The following flowchart illustrates the provisioning and boot process:</p>

<pre><code class="language-mermaid">flowchart TD
    
    %% Boot chain
    INITRAM[Load initramfs from boot.img] --&gt; KEYCHECK{"does a private key in the OTP exist?"}
    KEYCHECK -- No (do provisioning) --&gt; GENKEY[Generate key and store in OTP]
    GENKEY --&gt; ENCRYPTROOTFSB[create a LUKS container for &lt;span style='color:orange'&gt;rootfsB&lt;/span&gt; using the key from OTP]
    ENCRYPTROOTFSB --&gt; POPULATEB[Populate &lt;span style='color:orange'&gt;rootfsB&lt;/span&gt; with a copy of &lt;span style='color:green'&gt;rootfsA&lt;/span&gt;]
    POPULATEB --&gt; ENCRYPTROOTFSA[create a LUKS container for &lt;span style='color:green'&gt;rootfsA&lt;/span&gt; using key from OTP]
    ENCRYPTROOTFSA --&gt; POPULATEA[Populate &lt;span style='color:green'&gt;rootfsA&lt;/span&gt; with a copy from &lt;span style='color:orange'&gt;rootfsB&lt;/span&gt;]
    POPULATEA --&gt; MAP[Map all LUKS containers using key from OTP]
    MAP --&gt; MOUNT[Mount all LUKS containers]
    MOUNT --&gt; BOOT[Boot into selected rootfs]
    KEYCHECK -- Yes --&gt; MAP
    
</code></pre>

<p>The path for already provisioned devices is quite straight forward: Retrieve the encryption key from OTP memory, map the LUKS containers using <code class="language-plaintext highlighter-rouge">cryptsetup</code>, mount the root filesystem, and call <code class="language-plaintext highlighter-rouge">switch_root</code> to boot into the LUKS-encrypted root filesystem.</p>

<p>The provisioning process is more complex and requires several steps to create the LUKS-encrypted root filesystem and store the encryption key in OTP memory. Our script will perform the following steps during the provisioning process:</p>

<ol>
  <li>Detect if the device is already provisioned by checking if the encryption key is present in OTP memory and if the root filesystem partitions are already LUKS encrypted.</li>
  <li>Generate a unique encryption key for the device using a secure random number generator.</li>
  <li>Store the generated encryption key in the One-Time Programmable (OTP) memory of the Raspberry Pi CM4 using the appropriate tools (e.g., <code class="language-plaintext highlighter-rouge">rpi-eeprom</code>).</li>
  <li>Create a LUKS-encrypted container for the root filesystem using the generated encryption key.</li>
  <li>Copy the unencrypted root filesystem into the LUKS-encrypted container.</li>
  <li>Boot into the newly created LUKS-encrypted root filesystem.</li>
</ol>

<p>One question to clarify is where to store the unencrypted root filesystem before the provisioning process. Since LUKS does not support in-place encryption of existing filesystems, we need to have the unencrypted root filesystem available somewhere during the first boot. If you are using an A/B partition scheme, you can store the unencrypted root filesystem in one of the partitions (e.g., partition A) and use the other partition (e.g., partition B) for the LUKS-encrypted root filesystem. During the provisioning process, the init script can copy the unencrypted root filesystem from partition A into the LUKS-encrypted container on partition B and then do the same vice versa from the now encrypted B partition to A. While this complicates the init script somewhat, it streamlines the provisioning process significantly, as no external media is required to provide the unencrypted root filesystem during the first boot and the entire image can be flashed at once using <code class="language-plaintext highlighter-rouge">.wic</code> images or similar.</p>

<p>So let’s look at the key commands that need to be executed in the init script for both the provisioning and normal unlocking processes.</p>

<h4 id="provisioning-and-boot-process">Provisioning and boot process</h4>

<p>First we need to check if the device is already provisioned. The simplest way to do this is to check if all partitions are already LUKS encrypted and if there is a non-zero key stored in OTP memory. 
If the OTP key is not set, we need to generate a new encryption key and store it in OTP memory. The RPI eeprom tools provide a convenient way to do this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rpi-otp-private-key <span class="nt">-w</span> <span class="si">$(</span>openssl rand <span class="nt">-hex</span> 32<span class="si">)</span> <span class="nt">-y</span>
</code></pre></div></div>

<p>If the key is either generated or already present in OTP memory, we read it out and store it in a temporary key file for use with <code class="language-plaintext highlighter-rouge">cryptsetup</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rpi-otp-private-key <span class="nt">-r</span> <span class="o">&gt;</span> /keyfile.bin
<span class="c"># in case the private key is only zeroes, the file is not created</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-f</span> /keyfile.bin <span class="o">]</span> <span class="o">||</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-s</span> /keyfile.bin <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"[initramfs] FATAL ERROR: OTP private key file /keyfile.bin missing or empty. This is fatal!"</span>
    <span class="nb">exec</span> /bin/sh
<span class="k">fi</span>
</code></pre></div></div>

<p>Note, that on error we drop into a shell for debugging purposes, but in a production system you might want to <code class="language-plaintext highlighter-rouge">reboot -f</code> or take halt the system entirely to avoid leaving the device in an inconsistent or vulnerable state.</p>

<blockquote>
  <p>⚠️ Dropping into a shell on error is dangerous for production systems. Use with caution and only for debugging purposes. ⚠️</p>
</blockquote>

<p>Next we check if the root filesystem partitions are already LUKS encrypted or need to be provisioned. Here is an example of how to do this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">ROOTFS_A</span><span class="o">=</span><span class="s2">"/dev/mmcblk0p5"</span>   <span class="c"># rootfs_A</span>
<span class="nv">ROOTFS_B</span><span class="o">=</span><span class="s2">"/dev/mmcblk0p6"</span>   <span class="c"># rootfs_B</span>
<span class="nv">OTP_KEY_SET</span><span class="o">=</span><span class="nb">false
</span>rpi-otp-private-key <span class="nt">-c</span> <span class="o">&amp;&amp;</span> <span class="nv">OTP_KEY_SET</span><span class="o">=</span><span class="nb">true
</span>cryptsetup isLuks <span class="s2">"</span><span class="k">${</span><span class="nv">ROOTFS_B</span><span class="k">}</span><span class="s2">"</span> <span class="o">||</span> <span class="o">{</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">OTP_KEY_SET</span><span class="k">}</span><span class="s2">"</span> <span class="o">=</span> <span class="nb">false</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">ROOTFS_B_NEEDS_SETUP</span><span class="o">=</span><span class="nb">true</span><span class="p">;</span> <span class="o">}</span>
cryptsetup isLuks <span class="s2">"</span><span class="k">${</span><span class="nv">ROOTFS_A</span><span class="k">}</span><span class="s2">"</span> <span class="o">||</span> <span class="o">{</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">OTP_KEY_SET</span><span class="k">}</span><span class="s2">"</span> <span class="o">=</span> <span class="nb">false</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nv">ROOTFS_A_NEEDS_SETUP</span><span class="o">=</span><span class="nb">true</span><span class="p">;</span> <span class="o">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">rpi-otp-private-key -c</code> command stems from the <code class="language-plaintext highlighter-rouge">rpi-eeprom</code> yocto recipe and checks if a private key is stored in OTP memory. If the OTP is non-zero, it returns 0 else 1. We use this to set the <code class="language-plaintext highlighter-rouge">OTP_KEY_SET</code> variable accordingly. Next we check if the rootfs partitions are LUKS containers with the <code class="language-plaintext highlighter-rouge">cryptsetup isLuks</code> command. If either of the rootfs partitions is not a LUKS container and the OTP key is not set, we mark that partition as needing setup.</p>

<p>For this we create two helper functions: <code class="language-plaintext highlighter-rouge">set_up_luks_partition</code> and <code class="language-plaintext highlighter-rouge">provision_luks_partition</code>. The first function creates the LUKS container on the specified partition, while the second function copies the unencrypted root filesystem into the LUKS container and stores the encryption key in OTP memory. Here is an example implementation of these functions:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set_up_luks_partition<span class="o">(){</span>
    <span class="nv">target_partition</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
    <span class="nv">mapping_name</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>

    <span class="nb">echo</span> <span class="s2">"[initramfs] Setting up LUKS on </span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2">..."</span>
    cryptsetup luksFormat <span class="s2">"</span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2">"</span> <span class="nt">--batch-mode</span> <span class="nt">--key-file</span> /keyfile.bin <span class="o">||</span> <span class="o">{</span>
        <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to format LUKS partition </span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2">."</span>
        <span class="nb">exec</span> /bin/sh
    <span class="o">}</span>

    <span class="nb">echo</span> <span class="s2">"[initramfs] formatting </span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2"> to ext4."</span>

    cryptsetup luksOpen <span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span> <span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span> <span class="nt">--key-file</span> /keyfile.bin <span class="o">||</span> <span class="o">{</span>
        <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to open LUKS partition </span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2">."</span>
        <span class="nb">exec</span> /bin/sh
    <span class="o">}</span>   

    mkfs.ext4 /dev/mapper/<span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span> <span class="o">||</span> <span class="o">{</span>
        cryptsetup luksClose <span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span>
        <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to format LUKS partition </span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2">."</span>
        <span class="nb">exec</span> /bin/sh
    <span class="o">}</span>

    
    cryptsetup luksClose <span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span>
    <span class="nb">echo</span> <span class="s2">"[initramfs] LUKS partition </span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2"> is set up and ready."</span>
<span class="o">}</span>
</code></pre></div></div>

<p>First, the <code class="language-plaintext highlighter-rouge">set_up_luks_partition</code> function takes two arguments: the target partition to be encrypted and the mapping name for the LUKS container. It uses <code class="language-plaintext highlighter-rouge">cryptsetup luksFormat</code> to create the LUKS container on the specified partition, using a predefined key file (<code class="language-plaintext highlighter-rouge">/keyfile.bin</code>) for simplicity. After creating the LUKS container, it opens it with <code class="language-plaintext highlighter-rouge">cryptsetup luksOpen</code>, formats it to ext4 using <code class="language-plaintext highlighter-rouge">mkfs.ext4</code>, and then closes the LUKS container. If any of these steps fail, an error message is printed, and the system reboots. However depending on where things go wrong this might leave you with a bricked device, so be careful when testing this!</p>

<p>Once the LUKS container is set up, we can proceed to the provisioning process with the <code class="language-plaintext highlighter-rouge">provision_luks_partition</code> function:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>provision_luks_partition<span class="o">()</span> <span class="o">{</span>
    <span class="nv">target_partition</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
    <span class="nv">mapping_name</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>
    <span class="nv">source_partition</span><span class="o">=</span><span class="s2">"</span><span class="nv">$3</span><span class="s2">"</span>

    <span class="nb">echo</span> <span class="s2">"[initramfs] Opening LUKS partition </span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2">..."</span>
    cryptsetup luksOpen <span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span> <span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span> <span class="nt">--key-file</span> /keyfile.bin <span class="o">||</span> <span class="o">{</span>
        <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to open LUKS partition </span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2">."</span>
        <span class="nb">exec</span> /bin/sh
    <span class="o">}</span>   

    <span class="nb">echo</span> <span class="s2">"[initramfs] mounting </span><span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span><span class="s2"> to /mnt/target."</span>
    <span class="nb">mkdir</span> <span class="nt">-p</span> /mnt/target
    mount /dev/mapper/<span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span> /mnt/target <span class="o">||</span> <span class="o">{</span>
        cryptsetup luksClose <span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span>
        <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to mount LUKS partition </span><span class="k">${</span><span class="nv">target_partition</span><span class="k">}</span><span class="s2">."</span>
        <span class="nb">exec</span> /bin/sh
    <span class="o">}</span>
    <span class="nb">mkdir</span> <span class="nt">-p</span> /mnt/src
    mount <span class="k">${</span><span class="nv">source_partition</span><span class="k">}</span> /mnt/src <span class="o">||</span> <span class="o">{</span>
        
        umount /mnt/target
        cryptsetup luksClose <span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span>
        <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to mount source partition </span><span class="k">${</span><span class="nv">source_partition</span><span class="k">}</span><span class="s2">."</span>
        <span class="nb">exec</span> /bin/sh
    <span class="o">}</span>

    <span class="nb">echo</span> <span class="s2">"[initramfs] Copy everything from </span><span class="k">${</span><span class="nv">source_partition</span><span class="k">}</span><span class="s2"> (/mnt/src) to /mnt/target using rsync."</span>
    rsync <span class="nt">-aHAXx</span> /mnt/src/ /mnt/target/ <span class="o">||</span> <span class="o">{</span>
        umount /mnt/target
        cryptsetup luksClose <span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span>
        <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to copy data from /mnt/src to /mnt/target."</span>
        <span class="nb">exec</span> /bin/sh
    <span class="o">}</span>

    umount /mnt/target
    umount /mnt/src
    cryptsetup luksClose <span class="k">${</span><span class="nv">mapping_name</span><span class="k">}</span>

<span class="o">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">provision_luks_partition</code> function takes three arguments: the target partition to be provisioned, the mapping name for the LUKS container, and the source partition containing the unencrypted root filesystem. It opens the LUKS container on the target partition, mounts it to <code class="language-plaintext highlighter-rouge">/mnt/target</code>, and mounts the source partition to <code class="language-plaintext highlighter-rouge">/mnt/src</code>. It then uses <code class="language-plaintext highlighter-rouge">rsync</code> to copy all data from the source partition to the target LUKS-encrypted partition. After the data transfer is complete, it unmounts both partitions and closes the LUKS container. If any step fails, an error message is printed, and the system reboots.</p>

<p>We then can use these functions to handle the provisioning process during the first boot. Here is an example of how to use these functions in the init script:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">ROOTFS_B_NEEDS_SETUP</span><span class="k">}</span><span class="s2">"</span> <span class="o">=</span> <span class="nb">true</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span>set_up_luks_partition <span class="k">${</span><span class="nv">ROOTFS_B</span><span class="k">}</span> <span class="s2">"rootfs_b"</span> 
    provision_luks_partition <span class="k">${</span><span class="nv">ROOTFS_B</span><span class="k">}</span> <span class="s2">"rootfs_b"</span> <span class="k">${</span><span class="nv">ROOTFS_A</span><span class="k">}</span>
    
<span class="k">else
    </span><span class="nb">echo</span> <span class="s2">"[initramfs] Rootfs_B partition </span><span class="k">${</span><span class="nv">ROOTFS_B</span><span class="k">}</span><span class="s2"> is already LUKS formatted. Skipping setup and provisioning."</span>
<span class="k">fi</span>

<span class="c"># Create a LUKS container for mmcblk0p5 (rootfs_A) if needed</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">ROOTFS_A_NEEDS_SETUP</span><span class="k">}</span><span class="s2">"</span> <span class="o">=</span> <span class="nb">true</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"[initramfs] Rootfs_A partition </span><span class="k">${</span><span class="nv">ROOTFS_A</span><span class="k">}</span><span class="s2"> is not LUKS formatted. Setting up LUKS."</span>
    set_up_luks_partition <span class="k">${</span><span class="nv">ROOTFS_A</span><span class="k">}</span> <span class="s2">"rootfs_a"</span>
    <span class="c"># map rootfsb to copy data from it</span>
    cryptsetup luksOpen <span class="k">${</span><span class="nv">ROOTFS_B</span><span class="k">}</span> <span class="s2">"rootfs_b"</span> <span class="nt">--key-file</span> /keyfile.bin <span class="o">||</span> <span class="o">{</span>
        <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to open LUKS partition </span><span class="k">${</span><span class="nv">ROOTFS_B</span><span class="k">}</span><span class="s2">."</span>
        <span class="nb">exec</span> /bin/sh
    <span class="o">}</span>
    provision_luks_partition <span class="k">${</span><span class="nv">ROOTFS_A</span><span class="k">}</span> <span class="s2">"rootfs_a"</span> <span class="s2">"/dev/mapper/rootfs_b"</span>
    cryptsetup luksClose <span class="s2">"rootfs_b"</span>
<span class="k">else
    </span><span class="nb">echo</span> <span class="s2">"[initramfs] Rootfs_A partition </span><span class="k">${</span><span class="nv">ROOTFS_A</span><span class="k">}</span><span class="s2"> is already LUKS formatted. Skipping setup and provisioning."</span>
<span class="k">fi</span>
</code></pre></div></div>

<p>Note that while the first provisioning is done from an unencrypted partition to an encrypted one, the second provisioning is done from an already encrypted partition to another encrypted partition. For this we need to open the source LUKS container first and use the mapped device as source for the <code class="language-plaintext highlighter-rouge">provision_luks_partition</code> function.</p>

<p>Once the provisioning process is complete, we can proceed to unlock the LUKS-encrypted root filesystem for normal operation during subsequent boots. Here is an example of how to do this in the init script, for simplicity let’s say always into rootfs_A:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cryptsetup luksOpen <span class="k">${</span><span class="nv">ROOTFS_A</span><span class="k">}</span> rootfs_a <span class="nt">--key-file</span> /keyfile.bin <span class="o">||</span> <span class="o">{</span>
    <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to open LUKS partition </span><span class="k">${</span><span class="nv">ROOTFS_A</span><span class="k">}</span><span class="s2">."</span>
    <span class="nb">exec</span> /bin/sh
<span class="o">}</span>
mount /dev/mapper/rootfs_a /mnt/rootfs <span class="o">||</span> <span class="o">{</span>
    cryptsetup luksClose rootfs_a
    <span class="nb">echo</span> <span class="s2">"[initramfs] ERROR: Failed to mount LUKS partition </span><span class="k">${</span><span class="nv">ROOTFS_A</span><span class="k">}</span><span class="s2">."</span>
    <span class="nb">exec</span> /bin/sh
<span class="o">}</span>
</code></pre></div></div>

<p>At this point, the LUKS-encrypted root filesystem is unlocked and mounted to <code class="language-plaintext highlighter-rouge">/mnt/rootfs</code>, allowing the normal boot process to continue.</p>

<p>Finally, we need to switch to the unlocked root filesystem and continue the normal boot process. This can be done using the <code class="language-plaintext highlighter-rouge">switch_root</code> command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">exec </span>switch_root /mnt/rootfs /sbin/init
</code></pre></div></div>

<p>At this point, the init script has completed its tasks, and the normal boot process continues from the unlocked LUKS-encrypted root filesystem.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Getting an embedded device to support encryption at rest may look daunting at first, but as illustrated yocto makes it relatively straightforward to create the necessary initramfs image and modify the bootloader configuration. The more complex part is the provisioning process during the first boot, which requires careful handling of encryption keys and data transfer. While such a self-provisioning process may be comfortable to use, it might come with some security implications. First of all, until the provisioning is done, the unencrypted root filesystem is present on the device, which means that an attacker with physical access to the device could potentially read and modify the unencrypted data before the provisioning process is complete. Additionally, the provisioning process itself involves copying data from an unencrypted partition to an encrypted one, which could be vulnerable to interception or tampering if not properly secured. Therefore, it is crucial to ensure that the provisioning process is executed in a secure environment and that appropriate measures are taken to protect the unencrypted data during this phase. Using an external source to pull the unencrypted root filesystem during provisioning could mitigate some of these risks, but it also adds complexity to the provisioning process.</p>

<p>Another concern is that the provisioning code is still present in the initramfs even after the device has been provisioned. An attacker who gains access to the device could potentially exploit vulnerabilities in the provisioning code to compromise the security of the encrypted root filesystem. To mitigate this risk, it is advisable to implement a mechanism to disable or remove the provisioning code from the initramfs after the first successful boot and provisioning process. If your device supports update mechanisms, you could push an updated initramfs without the provisioning code after the first boot.</p>

<p>Despite these considerations, implementing encryption at rest using LUKS on embedded Linux devices based on the Raspberry Pi Compute Module 4 with the yocto Project provides a robust solution for protecting sensitive data and will make exploiting the device significantly harder, even if an attacker has physical access.</p>

<style>
    /* Default: Light theme → make box darker */
    .contrast-box {
        background-color: hsla(0, 0%, 94%, 0.918);
        margin-bottom: 1.5em;
        padding: 0.5em;
    }

    /* Dark theme → make box lighter */
    @media (prefers-color-scheme: dark) {

        .contrast-box {
            background-color: hsla(0, 0%, 89%, 0.055);
        }
    }
</style>

<div class="contrast-box">
    <h1><a href="/services/project-work/">
            Like what you read?</a></h1>

    <div style="display: grid; width: 100%; grid-template-columns: 10% 1fr; grid-gap: 1%; padding-bottom: 0.5em;">
        <div>

            <a href="/services/project-work/">
                <img src="/images/software-craft-logo_small.png" alt="SoftwareCraft Logo" style="max-width:100%;height:auto;display:block" />

            </a>

        </div>
        <div>
            We help teams design, build, and rescue complex software projects. From
            tricky architectures to hands-on delivery, we bring senior expertise where it matters most. Curious how we
            can help?
            <br />
            <br />

            <a href="/services/project-work/">Learn more</a> or
            <a href="/about/contact">or contact us</a>


        </div>

    </div>
</div>]]></content><author><name>Dominik Berner</name></author><category term="secure-boot" /><category term="yocto" /><category term="embedded-linux" /><summary type="html"><![CDATA[Implement LUKS encryption at rest on Raspberry Pi CM4 with yocto. Complete guide covering initramfs, cryptsetup, OTP keys, and self-provisioning setup.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/encryption-at-rest/encryption_at_rest_thumb.jpg" /><media:content medium="image" url="https://softwarecraft.ch//images/encryption-at-rest/encryption_at_rest_thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Bringing Secure Boot to Embedded Linux with Yocto</title><link href="https://softwarecraft.ch//secure-boot-yocto/" rel="alternate" type="text/html" title="Bringing Secure Boot to Embedded Linux with Yocto" /><published>2026-01-07T00:00:00+00:00</published><updated>2026-01-07T00:00:00+00:00</updated><id>https://softwarecraft.ch//secure-boot-yocto</id><content type="html" xml:base="https://softwarecraft.ch//secure-boot-yocto/"><![CDATA[<p><strong>Security is no longer optional for embedded devices, especially not with the Cyber Resilience Act coming into effect.</strong> Unfortunately, adding secure boot to embedded Linux devices is often not straightforward. In this post, we share our experience implementing secure boot for an embedded Linux device based on the Raspberry Pi Compute Module 4 using the Yocto Project. (Warning, this is a long read!)</p>

<h2 id="what-is-secure-boot">What is secure boot?</h2>

<p>There are many aspects to consider when building a secure embedded device. One of the first steps towards security is making sure that a device runs only trusted software. This is generally called <strong>Secure boot</strong> and it is preventing attackers from loading malicious code on the device during the boot process. Secure boot is typically implemented using a chain of trust, where each stage of the boot process verifies the integrity and authenticity of the next stage before executing it.</p>

<figure>
  <img src="/images/secure-boot-yocto/raspberry-secure-boot-process.png" alt="The Raspberry Pi 4 Boot Security Process" onclick="toggleSize(this)" />
  <figcaption>The Raspberry Pi 4 Boot Security Process (Click to enlarge, click again to reduce the size)
    
    <div class="source-annotation">Source: https://pip-assets.raspberrypi.com/categories/1260-security/documents/RP-004651-WP-2-Raspberry%20Pi%204%20Boot%20Security.pdf</div>
    
  </figcaption>
</figure>

<p>In practice, this means that the device’s bootloader is cryptographically signed, and the hardware verifies this signature before executing the bootloader. If the signature is invalid, the boot process is halted, preventing the execution of potentially malicious code. For a more in-depth documentation on secure boot for raspberry pi devices, check out the <a href="https://pip-assets.raspberrypi.com/categories/1260-security/documents/RP-004651-WP-2-Raspberry%20Pi%204%20Boot%20Security.pdf?disposition=inline">Raspberry Pi Secure Boot documentation</a>.</p>

<h2 id="implementing-secure-boot-with-yocto">Implementing secure boot with Yocto</h2>

<p>On a high level, implementing secure boot for an embedded Linux device using Yocto involves the following steps:</p>

<ul>
  <li>Enable trusted boot support on the device itself - In the case of the Raspberry Pi CM4, this involves configuring the EEPROM firmware to enable secure boot features.</li>
  <li>Provide the public key used for verifying the signature of the bootloader to the device and store it on the EEPROM.</li>
  <li>Create a <code class="language-plaintext highlighter-rouge">boot.img</code> container that includes the bootloader, kernel, and device tree blobs (DTBs) - This container needs to be signed using a public key.</li>
  <li>Configure Yocto to build and sign the <code class="language-plaintext highlighter-rouge">boot.img</code> - This involves creating custom Yocto layers and recipes to handle the signing process and ensure that the <code class="language-plaintext highlighter-rouge">boot.img</code> is correctly formatted.</li>
  <li>Replace existing boot files with the signed <code class="language-plaintext highlighter-rouge">boot.img</code> and pack it into the boot partition of the device’s storage medium.</li>
</ul>

<p>Before we dive into the details, we enable trusted boot on the Raspberry Pi CM4.</p>

<h3 id="enabling-trusted-boot-on-the-raspberry-pi-cm4">Enabling trusted boot on the Raspberry Pi CM4</h3>

<p>Trusted boot on the Raspberry Pi CM4 needs to be enabled in the boot EEPROM by setting the <code class="language-plaintext highlighter-rouge">SIGNED_BOOT</code> flag in the boot.conf and by providing the public key to verify the signature. This is done <a href="https://github.com/raspberrypi/usbboot/">using the <code class="language-plaintext highlighter-rouge">rpiboot</code> tool</a>, which allows us to write the necessary configuration to the EEPROM. The <a href="https://github.com/raspberrypi/usbboot/">usbboot repository</a> provides the necessary tools and documentations to update the EEPROM for Raspberry Pi.</p>

<p>For illustration purposes, this article shows how to enable secure boot using a self-signed key pair. In a production environment, you would typically use a key pair issued by a trusted certificate authority (CA).</p>

<p>First, we generate a public/private key pair using OpenSSL:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openssl genrsa 2048 <span class="o">&gt;</span> keypair.pem
</code></pre></div></div>

<p>Next, we set up the <code class="language-plaintext highlighter-rouge">boot.conf</code> file to enable signed boot by setting <code class="language-plaintext highlighter-rouge">SIGNED_BOOT=1</code>. Then we use the <code class="language-plaintext highlighter-rouge">update-pieeprom</code> tool from rpiboot to update the EEPROM with the public key and flash it to the device:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>update-pieeprom <span class="nt">-k</span>  keypair.pem
rpiboot <span class="nt">-d</span> /path/to/bootconf/
</code></pre></div></div>

<p>From this point on, the device will expect a signed <code class="language-plaintext highlighter-rouge">boot.img</code> during the boot process, the signature of it is expected to be present in a <code class="language-plaintext highlighter-rouge">boot.sig</code> file. If we try to boot an unsigned image, the device will halt the boot process with a <code class="language-plaintext highlighter-rouge">Bad signature boot.sig</code> error. To disable secure boot, we can overwrite the EEPROM again with <code class="language-plaintext highlighter-rouge">SIGNED_BOOT=0</code>.</p>

<p>Now that secure boot is enabled on the device, we can move on to configuring Yocto to build and sign the <code class="language-plaintext highlighter-rouge">boot.img</code>.</p>

<h3 id="configuring-yocto-to-build-and-sign-the-bootimg">Configuring Yocto to build and sign the boot.img</h3>

<p>Now that secure boot is enabled on the device and the key pair is generated, we can configure Yocto to build and sign the <code class="language-plaintext highlighter-rouge">boot.img</code>. This involves creating a custom Yocto recipe to create the boot container and to handle the signing process.</p>

<p>By default the raspberrypi layers in yocto store the boot files directly in the boot partition, so once we have a signed <code class="language-plaintext highlighter-rouge">boot.img</code>, we need to override the default behavior to use our signed image instead.</p>

<p>Let’s start with creating the <code class="language-plaintext highlighter-rouge">boot.img</code> container</p>

<h4 id="creating-the-bootimg-container">Creating the boot.img container</h4>

<p>The boot.img container is a fat16 formatted file that contains the bootloader, kernel, and device tree blobs (DTBs). We can create a custom Yocto recipe to build this container, let’s call this <code class="language-plaintext highlighter-rouge">boot-img-container.bb</code>:</p>

<p>The recipe will do the following things:</p>

<ul>
  <li>Create a staging directory to hold the boot files</li>
  <li>Copy the bootloader, kernel, and DTBs to the staging directory</li>
  <li>Create a fat16 formatted <code class="language-plaintext highlighter-rouge">boot.img</code></li>
  <li>Copy the boot files from the staging directory to the <code class="language-plaintext highlighter-rouge">boot.img</code></li>
</ul>

<p>Let’s look at the recipe code:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
inherit deploy nopackages

DEPENDS +<span class="o">=</span> <span class="s2">"rpi-bootfiles rpi-cmdline rpi-config linux-raspberrypi dosfstools-native mtools-native"</span>

<span class="c"># The raspberry pi bootloader files are located in the following directories</span>
BOOTFILES_DIR <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">WORKDIR</span><span class="k">}</span><span class="s2">/bootfiles"</span> 
DTBO_DIR <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span><span class="s2">"</span>

S <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">WORKDIR</span><span class="k">}</span><span class="s2">/boot-img-container"</span>

do_compile<span class="o">()</span> <span class="o">{</span>
    <span class="c"># Create staging and target directory</span>
    <span class="nb">mkdir</span> <span class="nt">-p</span> <span class="k">${</span><span class="nv">B</span><span class="k">}</span>/boot-img-container-staging/overlays
    <span class="nb">mkdir</span> <span class="nt">-p</span> <span class="k">${</span><span class="nv">S</span><span class="k">}</span>

    <span class="c">#collect all bootfiles from BOOTFILES_DIR and DTBO_DIR and and put it into the staging dir</span>
    find <span class="k">${</span><span class="nv">BOOTFILES_DIR</span><span class="k">}</span> <span class="nt">-maxdepth</span> 1 <span class="nt">-type</span> f <span class="o">!</span> <span class="nt">-name</span> <span class="s1">'cmdline.txt'</span> <span class="nt">-exec</span> <span class="nb">install</span> <span class="nt">-m</span> 0644 <span class="nt">-t</span> <span class="k">${</span><span class="nv">B</span><span class="k">}</span>/boot-img-container-staging/ <span class="o">{}</span> +

    <span class="c"># copy all resolved .dtbo symlinks (the image is vfat formatted and symlinks are not supported)</span>
    find <span class="k">${</span><span class="nv">DTBO_DIR</span><span class="k">}</span> <span class="nt">-type</span> f <span class="nt">-name</span> <span class="s1">'*.dtbo'</span> <span class="nt">-exec</span> <span class="nb">install</span> <span class="nt">-m</span> 0644 <span class="nt">-t</span> <span class="k">${</span><span class="nv">B</span><span class="k">}</span>/boot-img-container-staging/overlays <span class="o">{}</span> +

    <span class="c"># copy all dtb files except overlay_map.dtb</span>
    find <span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span> <span class="nt">-type</span> f <span class="nt">-name</span> <span class="s1">'*.dtb'</span> <span class="o">!</span> <span class="nt">-name</span> <span class="s1">'overlay_map.dtb'</span> <span class="nt">-exec</span> <span class="nb">install</span> <span class="nt">-m</span> 0644 <span class="nt">-t</span> <span class="k">${</span><span class="nv">B</span><span class="k">}</span>/boot-img-container-staging/ <span class="o">{}</span> +

    <span class="c"># the overlay map file needs to be placed into the overlays folder too</span>
    <span class="nb">install</span> <span class="nt">-m</span> 0644 <span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span>/overlay_map.dtb <span class="k">${</span><span class="nv">B</span><span class="k">}</span>/boot-img-container-staging/overlays/overlay_map.dtb

    <span class="c"># copy the kernel image</span>
    <span class="nb">install</span> <span class="nt">-m</span> 0644 <span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span>/<span class="k">${</span><span class="nv">KERNEL_IMAGETYPE_DIRECT</span><span class="k">}</span> <span class="k">${</span><span class="nv">B</span><span class="k">}</span>/boot-img-container-staging/<span class="k">${</span><span class="nv">SDIMG_KERNELIMAGE</span><span class="k">}</span>

    <span class="nv">BOOT_STAGING_DIR</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">B</span><span class="k">}</span><span class="s2">/boot-img-container-staging"</span>
    <span class="nv">BOOT_STAGING_SIZE_BYTES</span><span class="o">=</span><span class="si">$(</span>find <span class="s2">"</span><span class="nv">$BOOT_STAGING_DIR</span><span class="s2">"</span> <span class="nt">-type</span> f <span class="nt">-printf</span> <span class="s1">'%s\n'</span> 2&gt;/dev/null | <span class="nb">awk</span> <span class="s1">'{s+=$1} END{print s+0}'</span><span class="si">)</span>
    
    <span class="c"># Create a FAT image sized to fit the staged files (+ padding), format it, copy files.</span>
    <span class="nv">BOOT_IMG</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">S</span><span class="k">}</span><span class="s2">/boot.img"</span>

    <span class="c"># Add padding and round up to nearest MiB for filesystem overhead use awk because shell arithmetic is poorly supported in bitbake</span>
    <span class="nv">PADDING</span><span class="o">=</span><span class="si">$(</span><span class="nb">awk</span> <span class="s1">'BEGIN{printf "%d\n", 8*1024*1024}'</span><span class="si">)</span>
    <span class="nv">PADDED_SIZE</span><span class="o">=</span><span class="si">$(</span><span class="nb">awk</span> <span class="nt">-v</span> <span class="nv">a</span><span class="o">=</span><span class="s2">"</span><span class="nv">$BOOT_STAGING_SIZE_BYTES</span><span class="s2">"</span> <span class="nt">-v</span> <span class="nv">b</span><span class="o">=</span><span class="s2">"</span><span class="nv">$PADDING</span><span class="s2">"</span> <span class="s1">'BEGIN{printf "%d\n", a+b}'</span><span class="si">)</span>
    <span class="nv">MIN_SIZE</span><span class="o">=</span><span class="si">$(</span><span class="nb">awk</span> <span class="s1">'BEGIN{printf "%d\n", 1*1024*1024}'</span><span class="si">)</span>
    <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$PADDED_SIZE</span><span class="s2">"</span> <span class="nt">-lt</span> <span class="s2">"</span><span class="nv">$MIN_SIZE</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span><span class="nv">PADDED_SIZE</span><span class="o">=</span><span class="s2">"</span><span class="nv">$MIN_SIZE</span><span class="s2">"</span>
    <span class="k">fi
    </span><span class="nv">PADDED_SIZE</span><span class="o">=</span><span class="si">$(</span><span class="nb">awk</span> <span class="nt">-v</span> <span class="nv">n</span><span class="o">=</span><span class="s2">"</span><span class="nv">$PADDED_SIZE</span><span class="s2">"</span> <span class="s1">'BEGIN{s=1024*1024; printf "%d\n", int((n + s - 1) / s) * s }'</span><span class="si">)</span>

    <span class="c"># Create an empty file of the desired size</span>
    <span class="nb">rm</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$BOOT_IMG</span><span class="s2">"</span>
    <span class="nb">truncate</span> <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$PADDED_SIZE</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$BOOT_IMG</span><span class="s2">"</span>

    <span class="c"># Create FAT16 filesystem</span>
    mkfs.vfat <span class="nt">-F</span> 16 <span class="nt">-n</span> BOOT <span class="s2">"</span><span class="nv">$BOOT_IMG</span><span class="s2">"</span>

    <span class="c"># Copy files to the FAT image</span>
    mcopy <span class="nt">-i</span> <span class="k">${</span><span class="nv">BOOT_IMG</span><span class="k">}</span> <span class="nt">-s</span> <span class="k">${</span><span class="nv">BOOT_STAGING_DIR</span><span class="k">}</span>/<span class="k">*</span> ::/

<span class="o">}</span>
do_compile[depends] +<span class="o">=</span> <span class="s2">"rpi-bootfiles:do_deploy"</span>
do_compile[depends] +<span class="o">=</span> <span class="s2">"rpi-cmdline:do_deploy"</span>
do_compile[depends] +<span class="o">=</span> <span class="s2">"rpi-config:do_deploy"</span>
<span class="c"># make do_compile depend on the boot files and kernel image</span>
do_compile[filedeps] +<span class="o">=</span> <span class="s2">" </span><span class="se">\</span><span class="s2">
    </span><span class="k">${</span><span class="nv">BOOTFILES_DIR</span><span class="k">}</span><span class="s2">/* </span><span class="se">\</span><span class="s2">
    </span><span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span><span class="s2">/*.dtb </span><span class="se">\</span><span class="s2">
    </span><span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span><span class="s2">/*.dtbo </span><span class="se">\</span><span class="s2">
    </span><span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span><span class="s2">/overlay_map.dtb </span><span class="se">\</span><span class="s2">
    </span><span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span><span class="s2">/</span><span class="k">${</span><span class="nv">KERNEL_IMAGETYPE_DIRECT</span><span class="k">}</span><span class="s2"> </span><span class="se">\</span><span class="s2">
</span></code></pre></div></div>

<p>First of all, we define the recipe as a deployment recipe by inheriting the <code class="language-plaintext highlighter-rouge">deploy</code> class. This allows us to place the generated <code class="language-plaintext highlighter-rouge">boot.img</code> in the deploy directory for later use. Since the recipe does not produce any packages to be put in the root filesystem, we also inherit the <code class="language-plaintext highlighter-rouge">nopackages</code> class. As for dependencies, we need the <code class="language-plaintext highlighter-rouge">rpi-bootfiles</code> and <code class="language-plaintext highlighter-rouge">linux-raspberrypi</code> recipes to provide the necessary boot files and kernel image. Additionally, we need <code class="language-plaintext highlighter-rouge">dosfstools-native</code> and <code class="language-plaintext highlighter-rouge">mtools-native</code> to create and manipulate the FAT filesystem.</p>

<p>Next we define the directories where the boot files are located. The <code class="language-plaintext highlighter-rouge">BOOTFILES_DIR</code> variable points to the directory where the Raspberry Pi bootloader files are located, while the <code class="language-plaintext highlighter-rouge">DTBO_DIR</code> variable points to the deploy directory where the DTBs are located.</p>

<p>And now begins the actual work. While we do not generate packages, we still compile an artifact, so we implement the <code class="language-plaintext highlighter-rouge">do_compile</code> function.</p>

<p>After creation of the staging and the target directory, we collect all necessary boot files from the defined directories and copy them to the staging directory. We make sure to copy the resolved <code class="language-plaintext highlighter-rouge">.dtbo</code> files instead of the symlinks, since the FAT filesystem does not support symlinks. These are all the <code class="language-plaintext highlighter-rouge">find ... -exec install ...</code> commands in the recipe. We also copy the kernel image to the staging directory.</p>

<p>Next comes the actual creation of the <code class="language-plaintext highlighter-rouge">boot.img</code>. We first calculate the size needed for the image by summing up the sizes of all files in the staging directory. We then add some padding and round up to the nearest MiB to account for filesystem overhead. This is a bit of an optimization to avoid wasting space, for a first implementation you could also just pick a fixed size that is large enough to hold all files. Since shell arithmetic is poorly supported in bitbake, I used <code class="language-plaintext highlighter-rouge">awk</code> for the calculations.</p>

<p>With the calculated size, we create an empty file of that size using <code class="language-plaintext highlighter-rouge">truncate</code>, format it as a FAT16 filesystem using <code class="language-plaintext highlighter-rouge">mkfs.vfat</code>, and finally copy all files from the staging directory to the <code class="language-plaintext highlighter-rouge">boot.img</code> using <code class="language-plaintext highlighter-rouge">mcopy</code>.</p>

<p>And with that, we have our <code class="language-plaintext highlighter-rouge">boot.img</code> container created!</p>

<p>Lastly, we make sure that the <code class="language-plaintext highlighter-rouge">do_compile</code> task depends on the deployment of the boot files and kernel image, so that it is executed after those are available. We also add the boot files and kernel image as file dependencies to ensure that any changes to these files will trigger a rebuild of the <code class="language-plaintext highlighter-rouge">boot.img</code>.</p>

<p>Next up is signing the <code class="language-plaintext highlighter-rouge">boot.img</code>.</p>

<h4 id="signing-the-bootimg">Signing the boot.img</h4>

<p>After creating the <code class="language-plaintext highlighter-rouge">boot.img</code>, we need to sign it using our private key. For debugging purposes, it is convenient to also be able to create unsigned images, so let’s add the signing as a separate recipe that depends on the <code class="language-plaintext highlighter-rouge">boot-img-container</code> recipe. This way we can choose whether to build a signed or unsigned image by including the appropriate recipe in our build.</p>

<p>Signing is relatively straightforward:</p>

<ul>
  <li>Find the generated <code class="language-plaintext highlighter-rouge">boot.img</code> from the <code class="language-plaintext highlighter-rouge">boot-img-container</code> recipe</li>
  <li>Generate a sha256 hash of the <code class="language-plaintext highlighter-rouge">boot.img</code> to create a digest</li>
  <li>Sign the digest using the private key to create the signature</li>
  <li>Copy the <code class="language-plaintext highlighter-rouge">boot.sig</code> file to the deploy directory</li>
</ul>

<p>The resulting recipe could look like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
inherit nopackages

DEPENDS +<span class="o">=</span> <span class="s2">"boot-img-container openssl-native xxd-native"</span>

BOOT_IMG_PATH <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span><span class="s2">/boot.img"</span>
SIG_FILE_PATH <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">DEPLOY_DIR_IMAGE</span><span class="k">}</span><span class="s2">/boot.sig"</span>
<span class="c"># Path to the .pem file containing the private key for signing </span>
BOOT_IMG_CERTIFICATE_PEM <span class="o">=</span> <span class="s2">"</span><span class="k">${</span><span class="nv">THISDIR</span><span class="k">}</span><span class="s2">/files/development-1.key.pem"</span>

do_compile<span class="o">()</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BOOT_IMG_CERTIFICATE_PEM</span><span class="k">}</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span>bbfatal <span class="s2">"BOOT_IMG_CERTIFICATE_PEM is not set. Please specify the path to the .pem file."</span>
    <span class="k">fi

    if</span> <span class="o">[</span> <span class="nt">-f</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BOOT_IMG_PATH</span><span class="k">}</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
        <span class="c"># Generate a signature file containing the sha256 hash of boot.img</span>
        <span class="nb">sha256sum</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BOOT_IMG_PATH</span><span class="k">}</span><span class="s2">"</span> | <span class="nb">awk</span> <span class="s1">'{print $1}'</span> <span class="o">&gt;</span> <span class="s2">"</span><span class="k">${</span><span class="nv">SIG_FILE_PATH</span><span class="k">}</span><span class="s2">"</span>
        <span class="c"># next is the time of signing </span>
        <span class="nb">printf</span> <span class="s1">'ts: %s\n'</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">date</span> <span class="nt">-u</span> +%s<span class="si">)</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="k">${</span><span class="nv">SIG_FILE_PATH</span><span class="k">}</span><span class="s2">"</span>

        <span class="c"># Generate RSA signature of the boot.img using openssl and hash it using sha256</span>
        <span class="c"># append at the end of the sig file</span>

        <span class="nv">sig</span><span class="o">=</span><span class="si">$(</span>openssl dgst <span class="nt">-sha256</span> <span class="nt">-sign</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BOOT_IMG_CERTIFICATE_PEM</span><span class="k">}</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BOOT_IMG_PATH</span><span class="k">}</span><span class="s2">"</span> | xxd <span class="nt">-c</span> 4096 <span class="nt">-p</span> <span class="si">)</span>
        <span class="nb">printf</span> <span class="s1">'rsa2048: %s\n'</span> <span class="s2">"</span><span class="k">${</span><span class="nv">sig</span><span class="k">}</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="k">${</span><span class="nv">SIG_FILE_PATH</span><span class="k">}</span><span class="s2">"</span>
    <span class="k">else
        </span>bbwarn <span class="s2">"boot.img not found, skipping signature generation."</span>
    <span class="k">fi</span>
<span class="o">}</span>

do_deploy<span class="o">()</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">[</span> <span class="nt">-f</span> <span class="s2">"</span><span class="k">${</span><span class="nv">SIG_FILE_PATH</span><span class="k">}</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">install</span> <span class="nt">-D</span> <span class="nt">-m</span> 0644 <span class="s2">"</span><span class="k">${</span><span class="nv">SIG_FILE_PATH</span><span class="k">}</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">D</span><span class="k">}${</span><span class="nv">DEPLOYDIR</span><span class="k">}</span><span class="s2">/boot.sig"</span>
    <span class="k">else
        </span>bbwarn <span class="s2">"Signature file not found, skipping installation."</span>
    <span class="k">fi</span>
<span class="o">}</span>

addtask do_compile after do_fetch before do_install
addtask do_deploy after do_compile before do_build
do_compile[depends] +<span class="o">=</span> <span class="s2">"boot-img-container:do_deploy"</span>


</code></pre></div></div>

<p>This recipe inherits the <code class="language-plaintext highlighter-rouge">nopackages</code> class since it does not produce any packages. It depends on the <code class="language-plaintext highlighter-rouge">boot-img-container</code> recipe to ensure that the <code class="language-plaintext highlighter-rouge">boot.img</code> is available for signing. Additionally, it depends on <code class="language-plaintext highlighter-rouge">openssl-native</code> and <code class="language-plaintext highlighter-rouge">xxd-native</code> to perform the signing operation.</p>

<p>The <code class="language-plaintext highlighter-rouge">BOOT_IMG_PATH</code> variable points to the location of the generated <code class="language-plaintext highlighter-rouge">boot.img</code>, while the <code class="language-plaintext highlighter-rouge">SIG_FILE_PATH</code> variable defines where the signature file will be created. The <code class="language-plaintext highlighter-rouge">BOOT_IMG_CERTIFICATE_PEM</code> variable should point to the private key file used for signing, this should be overridden with the actual path to the private key in a secure manner (i.e. by pulling it from an AWS Secrets Manager, Azure Key Vault, or similar secure storage).</p>

<p>The <code class="language-plaintext highlighter-rouge">do_compile</code> function first checks if the <code class="language-plaintext highlighter-rouge">BOOT_IMG_CERTIFICATE_PEM</code> variable is set and if the <code class="language-plaintext highlighter-rouge">boot.img</code> file exists. If both checks pass, it generates a SHA256 hash of the <code class="language-plaintext highlighter-rouge">boot.img</code> and writes it to the signature file. It also appends a timestamp to the signature file for reference. Then, it generates an RSA signature of the <code class="language-plaintext highlighter-rouge">boot.img</code> using the private key and appends it to the signature file.</p>

<p>And with that, we have our signed <code class="language-plaintext highlighter-rouge">boot.img</code> and <code class="language-plaintext highlighter-rouge">boot.sig</code> files ready for deployment! The <code class="language-plaintext highlighter-rouge">do_deploy</code> function installs the <code class="language-plaintext highlighter-rouge">boot.sig</code> file to the deploy directory, so the image creation command automatically packs it.</p>

<p>Lastly, we need to tell bitbake to execute the <code class="language-plaintext highlighter-rouge">do_compile</code> and <code class="language-plaintext highlighter-rouge">do_deploy</code> tasks at the appropriate times in the build process by adding them to the task graph. We do this by placing the <code class="language-plaintext highlighter-rouge">do_compile</code> task before the <code class="language-plaintext highlighter-rouge">do_install</code> task and the <code class="language-plaintext highlighter-rouge">do_deploy</code> task before the <code class="language-plaintext highlighter-rouge">do_build</code> task. We also make sure that the <code class="language-plaintext highlighter-rouge">do_compile</code> task depends on the deployment of the <code class="language-plaintext highlighter-rouge">boot-img-container</code> recipe to ensure that the <code class="language-plaintext highlighter-rouge">boot.img</code> is available for signing.</p>

<p>With that we have the recipes ready to create and sign the <code class="language-plaintext highlighter-rouge">boot.img</code>. The last step is to make sure that the signed image is used during the build process instead of the default boot files.</p>

<h4 id="using-the-signed-bootimg-in-the-build">Using the signed boot.img in the build</h4>

<p>Since the recipes are not packages, we add them to the machine part of the image configuration file (i.e. <code class="language-plaintext highlighter-rouge">raspberrypi-cm4.conf</code>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IMAGE_BOOT_FILES <span class="o">=</span> <span class="s2">"boot.img config.txt boot.sig"</span>
EXTRA_IMAGEDEPENDS +<span class="o">=</span> <span class="s2">"boot-img-container sign-boot-img"</span>
do_image_wic[depends] +<span class="o">=</span> <span class="s2">"sign-boot-img:do_compile"</span>
</code></pre></div></div>

<p>First the existing <code class="language-plaintext highlighter-rouge">IMAGE_BOOT_FILES</code> variable is overridden to include the <code class="language-plaintext highlighter-rouge">boot.img</code> and <code class="language-plaintext highlighter-rouge">boot.sig</code> files instead of the default boot files produced by the raspberrypi layer. Next, we add the <code class="language-plaintext highlighter-rouge">boot-img-container</code> and <code class="language-plaintext highlighter-rouge">sign-boot-img</code> recipes to the <code class="language-plaintext highlighter-rouge">EXTRA_IMAGEDEPENDS</code> variable to ensure that they are built before the image is created.</p>

<p>The recipes to create and sign the boot image need to be executed before the image creation process, so we add a dependency from the <code class="language-plaintext highlighter-rouge">do_image_wic</code> task to the <code class="language-plaintext highlighter-rouge">do_compile</code> task of the <code class="language-plaintext highlighter-rouge">sign-boot-img</code> recipe. If you are using a different image creation method (i.e. <code class="language-plaintext highlighter-rouge">do_image_sdcard</code>), you need to adjust this accordingly.</p>

<p>And with that, we have successfully integrated secure boot into our Yocto build process for the Raspberry Pi CM4. You can now build your image as usual using bitbake, and the resulting image will include the signed <code class="language-plaintext highlighter-rouge">boot.img</code> and <code class="language-plaintext highlighter-rouge">boot.sig</code> files, ensuring that only trusted software is executed during the boot process.</p>

<h2 id="what-about-further-improvements">What about further improvements?</h2>

<p>The recipes and approach described in this post provide a basic implementation of secure boot for an embedded Linux device using Yocto. For production use one improvement that needs to be made is to securely manage the private key used for signing the <code class="language-plaintext highlighter-rouge">boot.img</code>. Storing the private key directly in the source code or build environment is not secure and can lead to compromise of the key. Instead, consider using a secure key management solution, such as a hardware security module (HSM) or a cloud-based key management service (KMS), to store and manage the private key.</p>]]></content><author><name>Dominik Berner</name></author><category term="secure-boot" /><category term="yocto" /><category term="embedded-linux" /><summary type="html"><![CDATA[Secure boot for an embedded Linux devices based on raspberry pi CM4 using the yocto project]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/secure-boot-yocto/secure-boot-yocto-thumb.jpg" /><media:content medium="image" url="https://softwarecraft.ch//images/secure-boot-yocto/secure-boot-yocto-thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Demystifying the Cyber Resilience Act: A pragmatic starting point</title><link href="https://softwarecraft.ch//cyber-resilience-act/" rel="alternate" type="text/html" title="Demystifying the Cyber Resilience Act: A pragmatic starting point" /><published>2025-11-07T00:00:00+00:00</published><updated>2025-11-07T00:00:00+00:00</updated><id>https://softwarecraft.ch//cyber-resilience-act</id><content type="html" xml:base="https://softwarecraft.ch//cyber-resilience-act/"><![CDATA[<p><strong>The EU Cyber Resilience Act (CRA) is set to become a landmark regulation for software security in Europe.</strong> With its <a href="https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng">planned enforcement starting in September 2026</a>, companies developing software or embedded devices must prepare to meet its requirements or risk losing access to the EU market. While this sounds daunting at first, it might not be as overwhelming as it seems. This post provides a practical overview of what the CRA entails, what companies need to establish, and how to get started to meet the requirements.</p>

<h2 id="the-cyber-resilience-act-at-a-glance">The Cyber Resilience Act at a glance</h2>

<p>At the highest level, the Cyber Resilience Act (CRA) is a regulation by the European Union that aims to enhance the cybersecurity of products with digital elements, including software and connected hardware devices. The CRA establishes baseline security requirements for these products throughout their lifecycle, from design and development to deployment and maintenance. The term <strong>“products with digital elements”</strong> is defined broadly and includes any product that relies on software to function, such as IoT devices, medical devices, industrial control systems, and general-purpose software applications.</p>

<p>At the core, the CRA focuses on three main areas:</p>

<ul>
  <li>Security-by-design: Integrate security measures into the product design and development process from the outset.</li>
  <li>Vulnerability management and incident response: Establish processes for identifying, reporting, and remediating vulnerabilities throughout the product lifecycle. Develop and maintain an incident response plan to address security incidents effectively.</li>
  <li>Compliance documentation: Maintain comprehensive documentation to demonstrate compliance with CRA requirements.</li>
</ul>

<figure>
  <img src="/images/cyber-resilience-act/cra_pillars.jpg" alt="Three main components for compliance with the Cyber Resilience Act: Security-by-design, Vulnerability management, Compliance documentation" onclick="toggleSize(this)" />
  <figcaption>Three main components for compliance with the Cyber Resilience Act: Security-by-design, Vulnerability management, Compliance documentation (Click to enlarge, click again to reduce the size)
    
  </figcaption>
</figure>

<p>Let’s look at some of the key requirements of the CRA in more detail.</p>

<h3 id="security-by-design">Security-by-design</h3>

<p>Security by design is a fundamental principle of the CRA and is often considered the most technical part. This means that security considerations must be part of the design and development process of software products. Companies need to implement secure coding practices, conduct regular security testing (e.g., static and dynamic analysis, fuzzing, penetration testing), and ensure that products are resilient against common cyber threats. Additionally, products must be designed to minimize attack surfaces and protect sensitive data. But what about existing products? Do they need to be redesigned from scratch? Not necessarily. The CRA recognizes that many products are already in the market and allows for a risk-based approach to security improvements.</p>

<blockquote>
  <p>The CRA recognizes that many products are already in the market and allows for a risk-based approach to security improvements.</p>
</blockquote>

<p>This means companies need to assess the security and perform a threat analysis of their existing products first. After this assessment, they can prioritize security enhancements based on the identified risks and start implementing necessary measures to improve security over time. An important point here is that many security issues might be mitigated by means other than code changes, e.g., by limiting exposure to threat vectors or improving user guidance.</p>

<h3 id="vulnerability-management-and-incident-response">Vulnerability management and incident response</h3>

<p>The authors of the CRA understand that no software is perfect and vulnerabilities will inevitably be discovered over time. Therefore, the CRA mandates that companies establish vulnerability management and incident response processes. At the core, vulnerability management means setting up a way to report and triage vulnerabilities, a way to inform customers about the existence of a vulnerability, and ensuring timely remediation of identified vulnerabilities. Again, this sounds like a lot of work, but in many cases existing processes for receiving user feedback or bug reports can be extended to cover security vulnerabilities.</p>

<p>While proactively addressing vulnerabilities is important, companies must also be prepared to respond effectively when security incidents occur. This is closely tied to vulnerability management and may even use some of the same facilities.</p>

<blockquote>
  <p>Incident response means to have a clear plan in place that outlines roles, responsibilities, and communication channels during a security incident.</p>
</blockquote>

<p>The most important part of incident response is to have a clear plan in place that outlines roles, responsibilities, and communication channels during a security incident. Make it explicit who is taking the lead, who needs to sit in the crisis room, and what response times are expected. Who can make decisions to enable containing the incident, mitigating its impact, and recovering normal operations. The third part to be ready for the CRA is comprehensive compliance documentation.</p>

<h3 id="compliance-documentation">Compliance documentation</h3>

<p>Chances are that if you are affected by the CRA, you already have some level of compliance documentation in place, because many other regulations (e.g., GDPR, industry-specific regulations) also require documentation of security practices. If so, good news: you can build upon your existing documentation efforts. If you have not yet started, don’t panic; creating the necessary documentation is not an unmanageable task. So what kind of documentation is required? First, the efforts described in the previous sections need to be documented. This includes documenting the security-by-design practices, vulnerability management processes, and incident response plans discussed earlier.</p>

<blockquote>
  <p>Don’t panic, creating the necessary documentation for the CRA is not an unmanageable task.</p>
</blockquote>

<p>Additionally, companies must maintain records of security testing results, risk assessments, and any security-related decisions made during the product lifecycle. In practice this means nothing more than keeping the CI/CD and testing results of your releases ready and creating a comprehensive changelog. Additionally, the CRA requires companies to keep track of third-party components used in their products, including open-source libraries. This means maintaining a Software Bill of Materials (SBOM) that lists all components, their versions, and any known vulnerabilities associated with them. This can be automated to a large extent using existing tools that generate SBOMs as part of the build process.</p>

<p>The compliance documentation is the final piece of the puzzle to be ready for the CRA. While security-by-design and vulnerability management are the more practical parts, compliance documentation ensures that companies can demonstrate their adherence to CRA requirements. So where to start?</p>

<h2 id="get-started-with-the-cyber-resilience-act">Get started with the Cyber Resilience Act</h2>

<p>Preparing for the CRA may seem overwhelming at first, but breaking it down into manageable steps can make the process more approachable. An incremental approach is often the best way to build up the necessary capabilities to pass an audit and get your products certified.</p>

<p>Start with a high-level gap assessment to identify where your current practices stand in relation to the CRA requirements. Then implement a “CRA-Light” process that covers the most critical aspects of the CRA without overwhelming your teams. Start with the “who” regarding vulnerability management and incident response. Do a high-level STRIDE threat model for your main products to identify the biggest risks. Automate SBOM generation in your build process and start collecting compliance documentation as you go along.</p>

<p>While this might not cover all aspects of the CRA right away, it establishes a foundation that can be built upon over time. Building up CRA compliance incrementally allows teams to adapt and improve their processes without feeling overwhelmed by the full scope of the regulation from the start. And remember, passing audits on the first try is nice, but often not the case—so build a process that allows you to continuously improve over time.</p>

<p>After all, the CRA is not just about compliance; it’s about enhancing the overall security posture of products and protecting users from cyber threats. By taking proactive steps to meet CRA requirements, companies can not only ensure market access in the EU but also contribute to a safer digital ecosystem.</p>]]></content><author><name></name></author><category term="cyber-resilience-act" /><summary type="html"><![CDATA[A practical guide to the EU Cyber Resilience Act: key requirements, security-by-design, vulnerability management, incident response, SBOM & incremental compliance.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/cyber-resilience-act/thumbnail.jpg" /><media:content medium="image" url="https://softwarecraft.ch//images/cyber-resilience-act/thumbnail.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">std::expected in C++23: A Better Way to Handle Errors</title><link href="https://softwarecraft.ch//cpp-std-expected/" rel="alternate" type="text/html" title="std::expected in C++23: A Better Way to Handle Errors" /><published>2025-10-20T00:00:00+00:00</published><updated>2025-10-20T00:00:00+00:00</updated><id>https://softwarecraft.ch//cpp-std-expected</id><content type="html" xml:base="https://softwarecraft.ch//cpp-std-expected/"><![CDATA[<p><strong>How to handle errors in C++ has been a constant point of debate.</strong> Do you use exceptions, error code, out-parameters or return nullptrs on failure? And how do you convey information on the nature of the failure? With C++17 we got <code class="language-plaintext highlighter-rouge">std::optional</code> for “value or nothing” semantics, but it lacks error context. <a href="https://en.cppreference.com/w/cpp/utility/expected.html">C++23 - finally - introduces <code class="language-plaintext highlighter-rouge">std::expected</code></a>, a type that encapsulates either a value or an error, making error handling explicit and composable. Let’s explore how <code class="language-plaintext highlighter-rouge">std::expected</code> can improve your C++ code.</p>

<h2 id="stdexpected-in-a-nutshell"><code class="language-plaintext highlighter-rouge">std::expected</code> in a nutshell</h2>

<p>At the core, <code class="language-plaintext highlighter-rouge">std::expected&lt;T, E&gt;</code> is a returnable type that can either hold a value of type <code class="language-plaintext highlighter-rouge">T</code> (indicating success) or an error of type <code class="language-plaintext highlighter-rouge">E</code> (indicating failure). This makes it clear to the caller that a function can fail and provides a structured way to handle that failure.</p>

<p>Let’s look at a simple example of a function that computes the square root of a number, returning an error if the input is negative:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;expected&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;cmath&gt;</span><span class="cp">
</span>
<span class="n">std</span><span class="o">::</span><span class="n">expected</span><span class="o">&lt;</span><span class="kt">double</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">safe_sqrt</span><span class="p">(</span><span class="kt">double</span> <span class="n">x</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">x</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">unexpected</span><span class="p">(</span><span class="s">"Negative input"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">sqrt</span><span class="p">(</span><span class="n">x</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">...</span>

<span class="c1">// Usage</span>
<span class="k">const</span> <span class="k">auto</span> <span class="n">result</span> <span class="o">=</span> <span class="n">safe_sqrt</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Square root: "</span> <span class="o">&lt;&lt;</span> <span class="o">*</span><span class="n">result</span> <span class="o">&lt;&lt;</span> <span class="sc">'\n'</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"Error: "</span> <span class="o">&lt;&lt;</span> <span class="n">result</span><span class="p">.</span><span class="n">error</span><span class="p">()</span> <span class="o">&lt;&lt;</span> <span class="sc">'\n'</span><span class="p">;</span>
<span class="p">}</span>

</code></pre></div></div>
<p>In this example, <code class="language-plaintext highlighter-rouge">safe_sqrt</code> returns an <code class="language-plaintext highlighter-rouge">std::expected&lt;double, std::string&gt;</code>. If the input is valid, it returns the square root; otherwise, it returns an error message. The caller can then check if the result is valid and handle the error accordingly. So how does this compare to traditional error handling methods?</p>

<h3 id="comparison-to-traditional-error-handling">Comparison to Traditional Error Handling</h3>

<p>Before <code class="language-plaintext highlighter-rouge">std::expected</code>, there were typically two main approaches to error handling in C++: exceptions and error codes. While exceptions can be powerful, they typically bring with them more complexity in control flow and then there is the discussion which errors should cause an exception to be thrown and which should not. The benefit of exceptions is that they allow for clean separation of error handling code and for propagation of errors up the call stack. 
Error codes on the other hand tend to either clutter the code by requiring out-parameters or have the problem of being either ignored or misunderstood by the caller. While <a href="https://en.cppreference.com/w/cpp/language/attributes/nodiscard">nodiscard</a> can help with ignored return values, it still does not solve the problem that the caller has to semantically understand the meaning of the return value.</p>

<p><code class="language-plaintext highlighter-rouge">std::expected</code> provides a middle ground. It makes error handling explicit in the type system, allowing to pass semantic information about the error back to the caller. The beauty of <code class="language-plaintext highlighter-rouge">std::expected</code>is also, that it can help to discern between expected or recoverable errors (e.g. file not found, invalid input) and unexpected or unrecoverable errors (e.g. out of memory, logic errors) which should still be handled via exceptions.</p>

<blockquote>
  <p><strong>Tip:</strong> Use <code class="language-plaintext highlighter-rouge">std::expected</code> for recoverable errors where the caller can take action based on the error, and reserve exceptions for truly exceptional situations.</p>
</blockquote>

<p>Let’s look at a more complex example that demonstrates how <code class="language-plaintext highlighter-rouge">std::expected</code> can be used in a real-world scenario.</p>

<h3 id="real-world-example-reading-a-qr-code-from-an-image">Real world example: Reading a QR code from an image</h3>

<p>Let’s suppose we want to write a function that reads a QR code from binary image data. The function generally has three paths:</p>

<ol>
  <li>The image contains a valid QR code and we can return the decoded string.</li>
  <li>The image does not contain a QR code and we want to return an error indicating that.</li>
  <li>The image data is unreadable (e.g. corrupted or unrecognizable format) and we want to throw an exception.</li>
</ol>

<p>While the first two paths are expected and recoverable errors, the third path is an unexpected error that should be handled via exceptions. So the implementation could look like this:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;expected&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;string&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdexcept&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;vector&gt;</span><span class="c1">   </span><span class="cp">
</span>
<span class="n">std</span><span class="o">::</span><span class="n">expected</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span> <span class="n">read_qr_code</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">uint8_t</span><span class="o">&gt;</span> <span class="k">const</span><span class="o">&amp;</span> <span class="n">image_data</span><span class="p">)</span> <span class="p">{</span>
    
    <span class="k">if</span> <span class="p">(</span><span class="n">image_data</span><span class="p">.</span><span class="n">empty</span><span class="p">()</span> <span class="o">||</span> <span class="n">check_if_corrupted</span><span class="p">(</span><span class="n">image_data</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="n">std</span><span class="o">::</span><span class="n">invalid_argument</span><span class="p">(</span><span class="s">"Invalid image data"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// Assume parse_image_data is a function that parses the image data and returns the QR code string or throws on failure</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">parsed_data</span> <span class="o">=</span> <span class="n">parse_image_data</span><span class="p">(</span><span class="n">image_data</span><span class="p">);</span> <span class="c1">// May throw exceptions on failure</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">parsed_data</span><span class="p">.</span><span class="n">empty</span><span class="p">())</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">unexpected</span><span class="p">(</span><span class="s">"No QR code found"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">parsed_data</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Note that in this example, both the success and error type are strings, but they could be any type. In a lot of cases, it might still make sense to use an enum or a custom error type for the error case to make it more structured. However, by using <code class="language-plaintext highlighter-rouge">std::expected</code>, we already are able to add a lot more context to the function without cluttering the code. This already is a big improvement over returning, but there is more. <code class="language-plaintext highlighter-rouge">std::expected</code> also provides a set of powerful combinators for composing operations that may fail, allowing for more elegant error handling.</p>

<h3 id="monadic-chaining-with-and_then">Monadic chaining with <code class="language-plaintext highlighter-rouge">and_then</code></h3>

<p>One of the benefits of <code class="language-plaintext highlighter-rouge">std::expected</code> is that it allows for <a href="https://en.wikipedia.org/wiki/Monad_(functional_programming)">monadic chaining</a>, A technique widely used in functional programming. Monadic chaining lets you compose a sequence of operations that may fail, without deeply nested <code class="language-plaintext highlighter-rouge">if</code>/<code class="language-plaintext highlighter-rouge">else</code>. With <code class="language-plaintext highlighter-rouge">std::expected</code>, you chain:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">and_then</code> when the next step itself may fail and returns another <code class="language-plaintext highlighter-rouge">expected</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">transform</code> when the next step cannot fail and just maps the value.</li>
  <li><code class="language-plaintext highlighter-rouge">or_else</code> to act on or recover from an error.</li>
</ul>

<p>Below is a continuation of the QR example, showing a pipeline that:
1) reads the QR payload,
2) validates it,
3) parses it as a URI,
4) extracts the host (pure mapping),
5) adds context to the error if anything failed.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Reuse the earlier read_qr_code signature:</span>
<span class="c1">// std::expected&lt;std::string, std::string&gt; read_qr_code(const std::vector&lt;uint8_t&gt;&amp;);</span>

<span class="cp">#include</span> <span class="cpf">&lt;expected&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;string&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;vector&gt;</span><span class="cp">
</span>
<span class="k">struct</span> <span class="nc">Uri</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">scheme</span><span class="p">;</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">host</span><span class="p">;</span>
    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">path</span><span class="p">;</span>
<span class="p">};</span>

<span class="n">std</span><span class="o">::</span><span class="n">expected</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span>
<span class="n">validate_payload</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="k">const</span><span class="o">&amp;</span> <span class="n">s</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">empty</span><span class="p">())</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">unexpected</span><span class="p">(</span><span class="s">"Empty QR payload"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mi">4096</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// arbitrary sanity limit</span>
        <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">unexpected</span><span class="p">(</span><span class="s">"QR payload too large"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">s</span><span class="p">;</span> <span class="c1">// valid as-is</span>
<span class="p">}</span>

<span class="n">std</span><span class="o">::</span><span class="n">expected</span><span class="o">&lt;</span><span class="n">Uri</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span>
<span class="n">parse_uri</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="k">const</span><span class="o">&amp;</span> <span class="n">s</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">auto</span> <span class="n">starts_with</span> <span class="o">=</span> <span class="p">[</span><span class="o">&amp;</span><span class="p">](</span><span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="k">const</span><span class="o">&amp;</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">s</span><span class="p">.</span><span class="n">rfind</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">};</span>

    <span class="n">Uri</span> <span class="n">u</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">starts_with</span><span class="p">(</span><span class="s">"https://"</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">u</span><span class="p">.</span><span class="n">scheme</span> <span class="o">=</span> <span class="s">"https"</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="n">starts_with</span><span class="p">(</span><span class="s">"http://"</span><span class="p">))</span> <span class="p">{</span>
        <span class="n">u</span><span class="p">.</span><span class="n">scheme</span> <span class="o">=</span> <span class="s">"http"</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">unexpected</span><span class="p">(</span><span class="s">"Not an http(s) URI"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// very naive split: scheme://host/path</span>
    <span class="k">auto</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">"://"</span><span class="p">);</span>
    <span class="k">auto</span> <span class="n">rest</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">substr</span><span class="p">(</span><span class="n">pos</span> <span class="o">+</span> <span class="mi">3</span><span class="p">);</span>
    <span class="k">auto</span> <span class="n">slash</span> <span class="o">=</span> <span class="n">rest</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="sc">'/'</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">slash</span> <span class="o">==</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">::</span><span class="n">npos</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">u</span><span class="p">.</span><span class="n">host</span> <span class="o">=</span> <span class="n">rest</span><span class="p">;</span>
        <span class="n">u</span><span class="p">.</span><span class="n">path</span> <span class="o">=</span> <span class="s">"/"</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">u</span><span class="p">.</span><span class="n">host</span> <span class="o">=</span> <span class="n">rest</span><span class="p">.</span><span class="n">substr</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">slash</span><span class="p">);</span>
        <span class="n">u</span><span class="p">.</span><span class="n">path</span> <span class="o">=</span> <span class="n">rest</span><span class="p">.</span><span class="n">substr</span><span class="p">(</span><span class="n">slash</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">u</span><span class="p">.</span><span class="n">host</span><span class="p">.</span><span class="n">empty</span><span class="p">())</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">unexpected</span><span class="p">(</span><span class="s">"Missing host"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">u</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="nf">host_from</span><span class="p">(</span><span class="n">Uri</span> <span class="k">const</span><span class="o">&amp;</span> <span class="n">u</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">u</span><span class="p">.</span><span class="n">host</span><span class="p">;</span> <span class="c1">// pure mapping, cannot fail</span>
<span class="p">}</span>

<span class="n">std</span><span class="o">::</span><span class="n">expected</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span>
<span class="n">annotate_error</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="k">const</span><span class="o">&amp;</span> <span class="n">err</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">unexpected</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">{</span><span class="s">"QR processing failed: "</span><span class="p">}</span> <span class="o">+</span> <span class="n">err</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Usage: linear, early-exiting pipeline</span>
<span class="n">std</span><span class="o">::</span><span class="n">expected</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&gt;</span>
<span class="n">extract_qr_host</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">uint8_t</span><span class="o">&gt;</span> <span class="k">const</span><span class="o">&amp;</span> <span class="n">image</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">read_qr_code</span><span class="p">(</span><span class="n">image</span><span class="p">)</span>
        <span class="p">.</span><span class="n">and_then</span><span class="p">(</span><span class="n">validate_payload</span><span class="p">)</span>        <span class="c1">// may fail -&gt; expected&lt;Payload, E&gt;</span>
        <span class="p">.</span><span class="n">and_then</span><span class="p">(</span><span class="n">parse_uri</span><span class="p">)</span>               <span class="c1">// may fail -&gt; expected&lt;Uri, E&gt;</span>
        <span class="p">.</span><span class="n">transform</span><span class="p">(</span><span class="n">host_from</span><span class="p">)</span>              <span class="c1">// cannot fail -&gt; expected&lt;std::string, E&gt;</span>
        <span class="p">.</span><span class="n">or_else</span><span class="p">(</span><span class="n">annotate_error</span><span class="p">);</span>          <span class="c1">// act on error path, keep E the same</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As we see, the resulting code in <code class="language-plaintext highlighter-rouge">extract_qr_host</code> is linear and easy to read. Each step is clearly defined, and error handling is centralized without deeply nested conditionals. The use of <code class="language-plaintext highlighter-rouge">and_then</code>, <code class="language-plaintext highlighter-rouge">transform</code>, and <code class="language-plaintext highlighter-rouge">or_else</code> makes the intent of each operation explicit.</p>

<p>There are some pitfalls and good practices to keep in mind when using monadic chaining with <code class="language-plaintext highlighter-rouge">std::expected</code>:</p>

<ul>
  <li>Keep the error type <code class="language-plaintext highlighter-rouge">E</code> consistent across <code class="language-plaintext highlighter-rouge">and_then</code>/<code class="language-plaintext highlighter-rouge">or_else</code> steps. If you must change it, use <code class="language-plaintext highlighter-rouge">transform_error</code>.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">and_then</code> only with functions that return <code class="language-plaintext highlighter-rouge">expected&lt;..., E&gt;</code>. Use <code class="language-plaintext highlighter-rouge">transform</code> for pure mappings returning plain values.</li>
  <li>The chain short-circuits on the first error, returning that error downstream. So having a catch-all <code class="language-plaintext highlighter-rouge">or_else</code> at the end is a good practice.</li>
  <li>If a step can throw, those exceptions still propagate unless caught and converted to <code class="language-plaintext highlighter-rouge">std::unexpected</code>.</li>
  <li>Prefer passing by <code class="language-plaintext highlighter-rouge">const&amp;</code> in chain steps to avoid copies or use move semantics. However the guaranteed copy elision in C++17 and later often makes this less of a concern.</li>
</ul>

<p>With these practices in mind, <code class="language-plaintext highlighter-rouge">std::expected</code> and its combinators can greatly enhance the clarity and robustness of error handling in your C++ code.</p>

<h2 id="final-thoughts">final thoughts</h2>

<p>With the arrival of <code class="language-plaintext highlighter-rouge">std::expected</code> in C++23 there is another powerful tool in C++ to allow more expressive code in a functional programming style. This can make applications that do a lot of data processing and have many recoverable failure paths much cleaner and easier to maintain. While it does not replace exceptions for unrecoverable errors, it nicely complements them by providing a structured way to handle expected errors. And the beauty of it is, that it still works seamlessly with existing C++ code and libraries - So no need to go all in and change it everywhere. So give it a try in your next C++ project and see how it can improve your error handling!</p>

<hr />

<p>While <code class="language-plaintext highlighter-rouge">std::expected</code> became part of the C++ standard with C++23, it has been around for quite some time as an <a href="https://github.com/TartanLlama/expected">open-source library</a> by Sy Brand before that.</p>]]></content><author><name></name></author><category term="cpp" /><summary type="html"><![CDATA[Practical introduction to std::expected: intent-revealing, structured error handling for recoverable failure paths in modern C++.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/cpp_logo.png" /><media:content medium="image" url="https://softwarecraft.ch//images/cpp_logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building a local AI agent with Llama.cpp and C++</title><link href="https://softwarecraft.ch//getting-started-with-llamacpp/" rel="alternate" type="text/html" title="Building a local AI agent with Llama.cpp and C++" /><published>2025-09-11T00:00:00+00:00</published><updated>2025-09-11T00:00:00+00:00</updated><id>https://softwarecraft.ch//getting-started-with-llamacpp</id><content type="html" xml:base="https://softwarecraft.ch//getting-started-with-llamacpp/"><![CDATA[<p><strong>Large Language Models (LLMs) and AI agents are everywhere and there are tons of online services that let you use them.</strong> But what if you want to build your own, local AI agent that can run on your own hardware, without relying on cloud services? No problem - starting there is not as difficult as one might think. There is a great open source project called <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a> that makes it easy to run LLMs on your own hardware. Let’s see how to get stated with a simple AI agent using llama.cpp, CMake and C++.</p>

<h2 id="why-build-your-own-ai-agent">Why build your own AI agent?</h2>

<p>While tapping into an existing online service to build your custom AI agent might be convenient, there are several reasons why you might want to build your own. There might be privacy concerns, or you might need offline access to the agent or maybe you want to customize the agent to your specific needs. Building your own AI agent gives you full control over the model, the data and the functionality. You can tailor it to your specific use case and integrate it seamlessly into your existing systems. A great place to start building your own AI agent is the <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a> project.</p>

<p>Llama.cpp is a C/C++ implementation of Facebook’s LLaMA model that allows you to run LLMs on your own hardware. It is designed to be efficient and lightweight, making it suitable for running on a wide range of devices, from high-end servers to low-power edge devices. Llama.cpp supports various LLaMA models, including the smaller ones that can run on consumer-grade hardware. It offers a wide range of cross-platform support including various GPU backends for acceleration, but also runs on CPU only systems. So let’s see how to get started with llama.cpp.</p>

<h2 id="building-a-simple-ai-agent-with-llamacpp">Building a simple AI agent with llama.cpp</h2>

<p>So let’s build a simple executable that uses llama.cpp to answer simple prompts by using a pre-trained model. You can find the complete code on <a href="https://github.com/bernedom/LlamaPlayground">on GitHub it the bernedom/LlamaPlayground repository</a> which is inspired by the <a href="https://github.com/ggml-org/llama.cpp/tree/master/examples/simple">simple example provided in the llama.cpp repository</a>. The executable will be called <code class="language-plaintext highlighter-rouge">LLamaPlayground</code> and can be used like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; LLamaPlayground -m ./models/mymodel.gguf What is the capital of Switzerland?

The capital of Switzerland is Bern.
</code></pre></div></div>

<p>To get started we need a few things:</p>

<ul>
  <li>A C++17 compatible compiler and CMake</li>
  <li>llama.cpp as library, which we can get by using CMakes <code class="language-plaintext highlighter-rouge">FetchContent</code> module. (Unfortunately the conan module for llama.cpp is outdated at the time of writing)</li>
  <li>A LLM-Model as <code class="language-plaintext highlighter-rouge">.gguf</code> file which can be obtained from various sources, e.g. <a href="https://huggingface.co/models?search=gguf">Huggingface</a></li>
</ul>

<h3 id="setting-up-the-project">Setting up the project</h3>

<p>Let’s start by setting up a simple CMake project, that creates our executable and pulls in llama.cpp as a dependency. To kickstart your project with a basic CMake and dependency handling already set up, consider this <a href="https://github.com/bernedom/CMakeConanCatchTemplate">CMake/Conan/Catch2 Template</a>.</p>

<p>In the <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> we need to pull in llama.cpp using <code class="language-plaintext highlighter-rouge">FetchContent</code> and link it to our executable like this</p>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
<span class="nb">include</span><span class="p">(</span>FetchContent<span class="p">)</span>
<span class="nf">FetchContent_Declare</span><span class="p">(</span>
        llama
        GIT_REPOSITORY https://github.com/ggml-org/llama.cpp.git
        GIT_TAG master
<span class="p">)</span>
<span class="nf">FetchContent_MakeAvailable</span><span class="p">(</span>llama<span class="p">)</span>
...
</code></pre></div></div>
<p>This gives us access to the <code class="language-plaintext highlighter-rouge">llama</code> target that we can link to our executable like this <code class="language-plaintext highlighter-rouge">target_link_libraries(LLamaPlayground PRIVATE llama)</code>.</p>

<style>
    /* Default: Light theme → make box darker */
    .contrast-box {
        background-color: hsla(0, 0%, 94%, 0.918);
        margin-bottom: 1.5em;
        padding: 0.5em;
    }

    /* Dark theme → make box lighter */
    @media (prefers-color-scheme: dark) {

        .contrast-box {
            background-color: hsla(0, 0%, 89%, 0.055);
        }
    }
</style>

<div class="contrast-box">
    <h1><a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
            CMake Best Practices - The book</a></h1>

    <div style="display: grid; width: 100%; grid-template-columns: 25% 1fr; grid-gap: 1%; padding-bottom: 0.5em;">
        <div>

            <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
                <img src="/images/cmake-best-practices-2nd-edition.jpg" alt="Cover of the CMake Best Practices book 2nd Edition by Dominik Berner and Mustafa Kemal Gilor" style="max-width:100%;height:auto;display:block" />

            </a>

        </div>
        <div>
            CMake Best Practices: Discover proven techniques for creating and maintaining programming projects with
            CMake. Learn how to use CMake to maximum efficiency with this compendium of best practices for a lot of
            common tasks when building C++ software.
            <br />
            <br />
            <div class="order-button">
                <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">Get it from
                    Packt</a>
            </div>

        </div>
    </div>
</div>

<p>Next we need to obtain a model. You can find various models on <a href="https://huggingface.co/models?search=gguf">Huggingface</a>. For this example I downloaded the <code class="language-plaintext highlighter-rouge">ggml-org/gemma-3-1b-it-GGUF</code> model and placed it in a <code class="language-plaintext highlighter-rouge">models</code> folder next to the executable. This is a fairly lightweight model that accepts text or images as input and generates text as output.</p>

<p>The easiest way to get the model is to use the <a href="https://huggingface.co/docs/huggingface_hub/en/guides/cli">huggingface CLI</a>. You can install it using pip:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip install huggingface_hub[cli]
</code></pre></div></div>
<p>To download the model, you can use the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hf download ggml-org/gemma-3-1b-it-GGUF --local-dir ./models
</code></pre></div></div>

<p>And with that we are ready to start coding.</p>

<h3 id="coding-the-ai-agent">Coding the AI agent</h3>

<p>Our simple AI agent will have to do three basig things:</p>

<ol>
  <li><strong>Model Loading</strong>: Loading the pre-trained model file.</li>
  <li><strong>Prompt Tokenization</strong>: Converting the input prompt into tokens.</li>
  <li><strong>Text Generation</strong>: Using the model to predict and generate tokens.</li>
</ol>

<p>I’ll skip the boilerplate code to parse the arguments and focus on the llama.cpp specific parts, for the full code head over to the <a href="https://github.com/bernedom/LlamaPlayground">Github repository containing the example code</a></p>

<h4 id="model-loading">Model Loading</h4>

<p>First let’s load the model and initialize it.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">llama_model_params</span> <span class="n">model_params</span> <span class="o">=</span> <span class="n">llama_model_default_params</span><span class="p">();</span>
<span class="n">model_params</span><span class="p">.</span><span class="n">n_gpu_layers</span> <span class="o">=</span> <span class="n">ngl</span><span class="p">;</span>

<span class="n">llama_model</span> <span class="o">*</span><span class="n">model</span> <span class="o">=</span> <span class="n">llama_model_load_from_file</span><span class="p">(</span><span class="n">model_path</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">model_params</span><span class="p">);</span>
</code></pre></div></div>

<p>For simplicity we use the default model parameters, but you can customize them as needed. To illustrate how this works, the <code class="language-plaintext highlighter-rouge">n_gpu_layers</code> parameter specifies how many layers of the model should be offloaded to the GPU for acceleration. If you don’t have a GPU or want to run on CPU only, you can set this to <code class="language-plaintext highlighter-rouge">0</code>. That is already all the magic needed to load the model, lets go to the next step <strong>Prompt Tokenization</strong>.</p>

<h4 id="prompt-tokenization">Prompt Tokenization</h4>

<p>To convert the user-supplied prompt into tokens we need to convert it from a string into tokens that the model can understand. The prompt is tokenized using the model’s vocabulary. Tokenization involves splitting the input string into smaller units (tokens) that the model can process, these are no longer human readable strings but rather integer values that represent words or subwords.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">llama_vocab</span> <span class="o">*</span><span class="n">vocab</span> <span class="o">=</span> <span class="n">llama_model_get_vocab</span><span class="p">(</span><span class="n">model</span><span class="p">);</span>
<span class="k">const</span> <span class="kt">int</span> <span class="n">n_prompt</span> <span class="o">=</span> <span class="o">-</span><span class="n">llama_tokenize</span><span class="p">(</span><span class="n">vocab</span><span class="p">,</span> <span class="n">prompt</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">prompt</span><span class="p">.</span><span class="n">size</span><span class="p">(),</span> <span class="nb">nullptr</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">true</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>

<span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">llama_token</span><span class="o">&gt;</span> <span class="n">prompt_tokens</span><span class="p">(</span><span class="n">n_prompt</span><span class="p">);</span>
<span class="n">llama_tokenize</span><span class="p">(</span><span class="n">vocab</span><span class="p">,</span> <span class="n">prompt</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">prompt</span><span class="p">.</span><span class="n">size</span><span class="p">(),</span> <span class="n">prompt_tokens</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">prompt_tokens</span><span class="p">.</span><span class="n">size</span><span class="p">(),</span> <span class="nb">true</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>
</code></pre></div></div>

<p>The first call to <code class="language-plaintext highlighter-rouge">llama_tokenize</code> with a <code class="language-plaintext highlighter-rouge">nullptr</code> for the output tokens is used to determine how many tokens are needed for the prompt. It’s a particularity of llama.cpp that the first call returns a negative value, because it failed to write the tokens to a <code class="language-plaintext highlighter-rouge">nullptr</code>. The absolute value of the returned integer indicates the number of tokens required.</p>

<p>The second call actually performs the tokenization and fills the <code class="language-plaintext highlighter-rouge">prompt_tokens</code> vector with the resulting tokens. With this we can initialize the context for the model and prepare it for text generation.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">llama_context_params</span> <span class="n">ctx_params</span> <span class="o">=</span> <span class="n">llama_context_default_params</span><span class="p">();</span>
<span class="n">ctx_params</span><span class="p">.</span><span class="n">n_ctx</span> <span class="o">=</span> <span class="n">n_prompt</span> <span class="o">+</span> <span class="n">n_predict</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">ctx_params</span><span class="p">.</span><span class="n">n_batch</span> <span class="o">=</span> <span class="n">n_prompt</span><span class="p">;</span>

<span class="n">llama_context</span> <span class="o">*</span><span class="n">ctx</span> <span class="o">=</span> <span class="n">llama_init_from_model</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">ctx_params</span><span class="p">);</span>
</code></pre></div></div>

<p>The context defines the model’s working environment, including the number of tokens to process and the batch size. For our small example we set the context size to the sum of the prompt tokens and the number of tokens we want to predict. The batch size is set to the number of prompt tokens, which means that the model will process all prompt tokens in one go.</p>

<p>Now that we have the prompt tokenized, we can move on to the final step: <strong>Text Generation</strong>.</p>

<h4 id="text-generation">Text Generation</h4>

<p>The main loop evaluates the model and generates tokens iteratively. Each token is sampled, converted back to text, and appended to the output. for this we loop until we reach the desired number of predicted tokens or encounter an end-of-generation token.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">llama_batch</span> <span class="n">batch</span> <span class="o">=</span> <span class="n">llama_batch_get_one</span><span class="p">(</span><span class="n">prompt_tokens</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">prompt_tokens</span><span class="p">.</span><span class="n">size</span><span class="p">());</span>

<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">n_pos</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">n_pos</span> <span class="o">+</span> <span class="n">batch</span><span class="p">.</span><span class="n">n_tokens</span> <span class="o">&lt;</span> <span class="n">n_prompt</span> <span class="o">+</span> <span class="n">n_predict</span><span class="p">;)</span> <span class="p">{</span>
    <span class="n">llama_decode</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">batch</span><span class="p">);</span>

    <span class="n">llama_token</span> <span class="n">new_token_id</span> <span class="o">=</span> <span class="n">llama_sampler_sample</span><span class="p">(</span><span class="n">smpl</span><span class="p">,</span> <span class="n">ctx</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">llama_vocab_is_eog</span><span class="p">(</span><span class="n">vocab</span><span class="p">,</span> <span class="n">new_token_id</span><span class="p">))</span> <span class="k">break</span><span class="p">;</span>

    <span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mi">128</span><span class="p">];</span>
    <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="n">llama_token_to_piece</span><span class="p">(</span><span class="n">vocab</span><span class="p">,</span> <span class="n">new_token_id</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">n</span><span class="p">);</span>

    <span class="n">batch</span> <span class="o">=</span> <span class="n">llama_batch_get_one</span><span class="p">(</span><span class="o">&amp;</span><span class="n">new_token_id</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>First we fetch a batch of tokens to process, in our case this might be the entire prompt, but since we might want to extend to larger prompts, we enter a loop.</p>

<p>Inside the loop the current batch is decoded using <code class="language-plaintext highlighter-rouge">llama_decode</code>, which processes the tokens and updates the model’s internal state. Then we sample a new token using <code class="language-plaintext highlighter-rouge">llama_sampler_sample</code>, which selects the next token based on the model’s predictions. If the sampled token is an end-of-generation token, we break out of the loop.</p>

<p>The sampled token is then converted back to a human-readable string using <code class="language-plaintext highlighter-rouge">llama_token_to_piece</code> and printed to the console. Finally, we prepare the next batch containing just the newly generated token for the next iteration of the loop.
And that is all the code we need to build a simple AI agent using llama.cpp. You can find the complete code on <a href="https://github.com/bernedom/LlamaPlayground">on GitHub it the bernedom/LlamaPlayground repository</a>.</p>

<p>To run the AI agent, you need to compile the code using CMake and your C++ compiler. Make sure you have the model file in the specified path. Beware that even the smaller models can be quite memory/hungry, so make sure you have enough RAM available.</p>

<h3 id="whats-next">Whats next?</h3>

<p>This is a very simple example to get you started with llama.cpp and building your own AI agent. As the talk of edge AI and on-device AI is getting more and more popular, llama.cpp might be a great starting point to explore this field. There are of course many more things to explore be it towards more efficiency or hardware acceleration through CUDA or Vulkan or more advanced prompt handling and context management. Whether you find any practical use for it or just play around with it, I hope this post helped you to get started.</p>]]></content><author><name></name></author><category term="cpp" /><category term="ai" /><category term="cmake" /><summary type="html"><![CDATA[Learn how to build a local AI agent using llama.cpp and C++. This article covers setting up your project with CMake, obtaining a suitable LLM model, and implementing basic model loading, prompt tokenization, and text generation. Ideal for those interested in running AI models on their own hardware without relying on cloud services.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/llamacpp/thumbnail.jpg" /><media:content medium="image" url="https://softwarecraft.ch//images/llamacpp/thumbnail.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Faster build times with artifact based CI</title><link href="https://softwarecraft.ch//artifact-based-ci/" rel="alternate" type="text/html" title="Faster build times with artifact based CI" /><published>2025-08-21T00:00:00+00:00</published><updated>2025-08-21T00:00:00+00:00</updated><id>https://softwarecraft.ch//artifact-based-ci</id><content type="html" xml:base="https://softwarecraft.ch//artifact-based-ci/"><![CDATA[<p><strong>CI/CD pipelines are a standard part of many software development workflows.</strong> However, many teams still struggle with long build times, flaky tests and inefficient workflows. Apart from driving the cost of CI up, these issues can lead to frustration and reduced productivity up to quality issues as developers may not run the full test suites before deploying their changes. One approach to address these challenges is to use artifact based CI, which can significantly improve the efficiency and reliability of your CI/CD pipelines.</p>

<h2 id="what-is-artifact-based-ci">What is artifact based CI?</h2>

<p>Let’s look at a typical CI/CD pipeline: When a developer pushes code changes, the CI system triggers a build process that compiles the code, runs tests, and produces artifacts (e.g., binaries, libraries, documentation). In a traditional setup, every time a change is pushed, the entire build process is executed from scratch, which can be time-consuming and resource-intensive.</p>

<figure>
<img class="mermaid" src="https://mermaid.ink/svg/eyJjb2RlIjoiJSUtXG5mbG93Y2hhcnQgTFJcbkFbUHVzaCB0byB2ZXJzaW9uIGNvbnRyb2xdIC0tPiBCW0J1aWxkXSAtLT4gQ1tUZXN0XSAtLT4gRFtQYWNrYWdlXSAtLT4gRVtQdWJsaXNoXVxuJSUtIiwibWVybWFpZCI6bnVsbH0" />
<figcaption>
  A very high level view of a typical, trivial CI/CD pipeline
</figcaption>
</figure>

<p>For a simple and small project such a straight forward pipeline is all what it takes. However if projects grow larger and contain multiple components, the build process can become complex and time-consuming. This is where artifact based CI comes into play. A slightly more complicated example might be an application that consists of several modules, such as an executable and several libraries, where the executable depends on the libraries. A trivial approach to CI/CD would be to build all the libraries from scratch on every change, then deploy all together to a test-environment, run the tests and if successfully, package them and publish them somewhere or deploy them to the target systems.</p>

<figure>
<img class="mermaid" src="https://mermaid.ink/svg/eyJjb2RlIjoiJSUtXG5mbG93Y2hhcnQgTFJcbkFbUHVzaCB0byB2ZXJzaW9uIGNvbnRyb2xdIC0tPiBCW0J1aWxkIExpYnJhcnkgQV0gLS0-IENbQnVpbGQgTGlicmFyeSBCXSAtLT4gRFtCdWlsZCBleGVjdXRhYmxlXSAtLT4gRltUZXN0XSAtLT4gR1tQYWNrYWdlXSAtLT4gSFtQdWJsaXNoXVxuJSUtIiwibWVybWFpZCI6bnVsbH0" />
<figcaption>
  A more complex CI/CD pipeline with multiple components
</figcaption>
</figure>

<p>Since the libraries and the executable should be buildable individually, building everything from scratch, even if only a part of the code has changed is wasteful. Also, if for example only code in the executable changes, running the full library build and test process is not necessary. So to optimize this, we can introduce artifact based CI.</p>

<p>Artifact based CI is a practice where the output of your build process (the artifacts) are stored and reused in subsequent builds. This means that instead of rebuilding everything from scratch every time, we can leverage previously built artifacts, which can save a lot of time and resources.</p>

<p>If done right, artifact based CI can not just speed up your builds and increase the reliability of your CI/CD pipelines, but they are also a great way to test deployment processes on the go. By deploying the same artifacts that will be used in production, you can catch deployment issues early in the development process.</p>

<p>The same pipeline as before, but now with artifact based CI could look like this:</p>

<figure>
<img class="mermaid" src="https://mermaid.ink/svg/eyJjb2RlIjoiJSUtXG5mbG93Y2hhcnQgTFJcbkFbUHVzaCB0byB2ZXJzaW9uIGNvbnRyb2xdIC0tPiBBMXtEaWQgbGlicmFyeSBBIGNoYW5nZT99IC0tPnxZZXN8IEEyW0J1aWxkIExpYnJhcnkgQV0gLS0-IEEzW1N0b3JlIG5ldyBhcnRpZmFjdCBmb3IgTGlicmFyeSBBXVxuQntEaWQgTGlicmFyeSBCIGNoYW5nZT99IC0tPnxZZXN8IEIxW0J1aWxkIExpYnJhcnkgQl0gLS0-IEIyW1N0b3JlIG5ldyBhcnRpZmFjdCBmb3IgTGlicmFyeSBCXVxuJSUtXG5BIC0tPiBCXG5CIC0tPiB8Tm98IEN7RGlkIEV4ZWN1dGFibGUgY2hhbmdlP30gLS0-fFllc3wgQzFbUHVsbCBMaWJyYXJ5IEFydGlmYWN0c10tLT4gQzJbQnVpbGQgRXhlY3V0YWJsZV0gLS0-IEMzW1N0b3JlIG5ldyBhcnRpZmFjdCBmb3IgRXhlY3V0YWJsZV1cbkExIC0tPiB8Tm98IENcbkEzIC0tPiBDXG5CMiAtLT4gQ1xuQzMgLS0-IERcbkMgLS0-IHxOb3wgRFtQdWxsIGFydGlmYWN0c10gLS0-IEZbVGVzdF0gLS0-IEdbUGFja2FnZV0gLS0-IEhbUHVibGlzaF1cbiUlLSIsIm1lcm1haWQiOm51bGx9" />
<figcaption>
  The same pipeline as before, but now with artifact based CI
</figcaption>
</figure>

<p>Wow, this looks a lot more complicated, but let’s break it down a bit. The first thing introduced here is some form of decision making to determine whether a component has changed or not. This can often be done by checking the version control systems for changes in the relevant files or directories. What we also need is a way to store the artifacts produced by the build process. Many CI/CD systems such as github actions or gitlab pipelines offer a simple way to store and retrieve build outputs, but for more complex applications a dedicated artifact repository, such as JFrog Artifactory, Nexus Repository might be better suited. The key is to ensure that the artifacts are versioned and easily retrievable. 
The last thing to notice is that this already gives us a way to speed up the build process, even if we need to build everything from scratch, because we can parallelize the jobs for building the libraries.</p>

<p>When moving to artifact based CI, the central problem usually revolves around how to efficiently store and retrieve the artifacts, how to version them properly, and how to ensure that the right artifacts are used in the right places.</p>

<h3 id="finding-the-right-artifact">Finding the right artifact</h3>

<p>The core concept of artifact based CI rotates around storing and retrieving build artifacts efficiently. Looking a bit closer this the main problem to be solved is that we need to find the right artifact for the current build. So putting up a good versioning and naming scheme is crucial. In a typical workflow we have three kind of versions of artifacts that we need to consider:</p>

<ul>
  <li>
    <p><strong>Development builds</strong>: These are the artifacts produced during the development process, often for branches or pull requests. Typically, these builds are only used by a small group of developers (or even a single one) working on a specific feature or bug fix. They are usually stored temporarily and can be discarded after a certain period of time or when the feature is merged into the main branch.</p>
  </li>
  <li>
    <p><strong>Released builds</strong>: These are the artifacts that are deployed to production environments. They should be stored permanently and should be easily retrievable.</p>
  </li>
  <li>
    <p><strong>Staged builds</strong>: These are the artifacts used for testing purposes, such as staging or QA environments. They can be a mix of development and production builds, depending on the testing requirements. If all goes well, these artifacts might be promoted to production builds or discarded if newer artifacts are available.</p>
  </li>
</ul>

<p>The naming and versioning of the artifacts should reflect these different stages. A common approach is to use semantic versioning, where the version number consists of three parts: major, minor, and patch. For example, a development build might be named <code class="language-plaintext highlighter-rouge">myapp-1.0.0-dev-123</code>, where <code class="language-plaintext highlighter-rouge">123</code> is a build number, a commit hash, or a branch name (or a combination of all of them). A released build might be named <code class="language-plaintext highlighter-rouge">myapp-1.0.0</code>, and a staged build might be named <code class="language-plaintext highlighter-rouge">myapp-1.0.0-staging</code>.</p>

<p>This way it is easy to identify where an artifact comes from and what it is used for. If building for multiple platforms or configurations, the naming scheme can be extended to include the platform and configuration, such as <code class="language-plaintext highlighter-rouge">myapp-1.0.0-linux-x86_64</code>, <code class="language-plaintext highlighter-rouge">myapp-1.0.0-windows-x86_64</code>, etc.</p>

<p>For a start, a simple storage solution for these artifacts might be enough, but as the project grows, it is worth considering a dedicated artifact repository and a <a href="https://softwarecraft.ch/conan-as-cmake-dependency-provider/">package manager</a> to handle the artifacts. If you have the versioning and naming scheme covered, you are already a huge step further towards efficient artifact-based CI, and further improvements can usually be made incrementally.</p>

<style>
    /* Default: Light theme → make box darker */
    .contrast-box {
        background-color: hsla(0, 0%, 94%, 0.918);
        margin-bottom: 1.5em;
        padding: 0.5em;
    }

    /* Dark theme → make box lighter */
    @media (prefers-color-scheme: dark) {

        .contrast-box {
            background-color: hsla(0, 0%, 89%, 0.055);
        }
    }
</style>

<div class="contrast-box">
    <h1><a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
            CMake Best Practices - The book</a></h1>

    <div style="display: grid; width: 100%; grid-template-columns: 25% 1fr; grid-gap: 1%; padding-bottom: 0.5em;">
        <div>

            <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
                <img src="/images/cmake-best-practices-2nd-edition.jpg" alt="Cover of the CMake Best Practices book 2nd Edition by Dominik Berner and Mustafa Kemal Gilor" style="max-width:100%;height:auto;display:block" />

            </a>

        </div>
        <div>
            CMake Best Practices: Discover proven techniques for creating and maintaining programming projects with
            CMake. Learn how to use CMake to maximum efficiency with this compendium of best practices for a lot of
            common tasks when building C++ software.
            <br />
            <br />
            <div class="order-button">
                <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">Get it from
                    Packt</a>
            </div>

        </div>
    </div>
</div>

<h2 id="further-improvements">Further improvements</h2>

<p>Moving from a build-everything-all-the-time approach to an artifact-based CI approach can be a bit of a paradigm shift, but it can lead to significant improvements in build times and reliability. Once the CI pipeline is set up to handle artifacts, the door is open to further optimizations, such as caching dependencies, parallelizing builds, and more. Depending on the team’s setup, it can even be beneficial to make the artifacts available to developers locally, so they can test their changes against the same artifacts that will be used in production.</p>

<p>As always, the key is to start somewhere and iterate and improve over time. Shifting to artifact-based CI is a great step towards more efficient and reliable CI/CD pipelines, and it can help teams to focus on delivering value rather than dealing with long build times and waiting for feedback.</p>]]></content><author><name></name></author><category term="devops" /><category term="ci/cd" /><summary type="html"><![CDATA[Artifact-based CI can optimize your CI/CD pipelines by reducing build times, improving reliability, and streamlining workflows. Discover strategies for efficient artifact storage, versioning, and retrieval, and explore best practices for implementing artifact-based continuous integration in modern software development.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/artifact-based-ci/thumbnail.png" /><media:content medium="image" url="https://softwarecraft.ch//images/artifact-based-ci/thumbnail.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The 7 CMake anti-patterns that eat your time and energy</title><link href="https://softwarecraft.ch//most-common-cmake-anti-patterns/" rel="alternate" type="text/html" title="The 7 CMake anti-patterns that eat your time and energy" /><published>2025-07-07T00:00:00+00:00</published><updated>2025-07-07T00:00:00+00:00</updated><id>https://softwarecraft.ch//most-common-cmake-anti-patterns</id><content type="html" xml:base="https://softwarecraft.ch//most-common-cmake-anti-patterns/"><![CDATA[<p><strong>“CMake is hard and our builds are a nightmare!”</strong> If that sounds familiar, you’re not alone. CMake has a reputation for being painful to use - but most of that pain comes from bad practices, not the tool itself.In this post, I’ll break down 7 of the most common CMake anti-patterns I see in real projects. These issues often creep in from legacy setups or lack of modern CMake knowledge, and they tend to slow teams down, cause frustration, and make build systems nearly unmaintainable.</p>

<p>Let’s fix that.</p>

<h2 id="1-vendoring-everything-instead-of-using-find_package">1. Vendoring everything instead of using <code class="language-plaintext highlighter-rouge">find_package</code></h2>

<p>One of the worst time-wasters: bundling dependencies directly into your project (aka “vendoring”), either as source code or worse, as precompiled binaries.</p>

<p>It bloats your repo, makes updates a hassle, and creates fragile builds. Modern CMake projects should use <code class="language-plaintext highlighter-rouge">find_package</code> to locate and integrate dependencies cleanly. Many libraries now provide CMake config files out of the box. If they don’t, writing your own <code class="language-plaintext highlighter-rouge">Find&lt;Package&gt;.cmake</code> is a good step toward future-proofing.</p>

<style>
    /* Default: Light theme → make box darker */
    .contrast-box {
        background-color: hsla(0, 0%, 94%, 0.918);
        margin-bottom: 1.5em;
        padding: 0.5em;
    }

    /* Dark theme → make box lighter */
    @media (prefers-color-scheme: dark) {

        .contrast-box {
            background-color: hsla(0, 0%, 89%, 0.055);
        }
    }
</style>

<div class="contrast-box">
    <h1><a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
            CMake Best Practices - The book</a></h1>

    <div style="display: grid; width: 100%; grid-template-columns: 25% 1fr; grid-gap: 1%; padding-bottom: 0.5em;">
        <div>

            <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
                <img src="/images/cmake-best-practices-2nd-edition.jpg" alt="Cover of the CMake Best Practices book 2nd Edition by Dominik Berner and Mustafa Kemal Gilor" style="max-width:100%;height:auto;display:block" />

            </a>

        </div>
        <div>
            CMake Best Practices: Discover proven techniques for creating and maintaining programming projects with
            CMake. Learn how to use CMake to maximum efficiency with this compendium of best practices for a lot of
            common tasks when building C++ software.
            <br />
            <br />
            <div class="order-button">
                <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">Get it from
                    Packt</a>
            </div>

        </div>
    </div>
</div>

<p>Even better, tools like <a href="https://softwarecraft.ch/conan-as-cmake-dependency-provider/">Conan or vcpkg</a> can take care of dependency management for you. Stop hardcoding paths - let CMake and your package manager handle it.</p>

<h2 id="2-not-using-cmakepresets">2. Not using CMakePresets</h2>

<p><a href="https://softwarecraft.ch/cmake-presets-best-practices/">CMake Presets are a game-changer</a> - use them! Since their introduction, they’ve made managing multiple build configurations <em>so</em> much easier. Instead of fiddling with command-line flags or custom scripts, you can define builds cleanly in <code class="language-plaintext highlighter-rouge">CMakePresets.json</code>.</p>

<blockquote>
  <p>CMake presets are one of the most impactful features introduced in CMake since the introduction of targets.</p>
</blockquote>

<p>Presets reduce errors, improve consistency across teams, and help CI pipelines stay clean. By providing a standardized way to configure good build configurations for development, testing, and release builds, CMakePresets can significantly reduce the entry barrier for new contributors and help ensure that everyone is using the same build settings.  Always include and maintain presets for your project, and encourage contributors to use them or create their own <code class="language-plaintext highlighter-rouge">CMakeUserPresets.json</code> files for their specific needs.</p>

<h2 id="3-still-using-global-variables-instead-of-targets">3. Still using global variables instead of targets</h2>

<p>Modern CMake is all about <em>targets</em> - and has been for over a decade. Yet many projects still rely on global variables and configuration, making things messy and error-prone.</p>

<p>Targets make everything modular, scoped, and easier to maintain. Use commands like <code class="language-plaintext highlighter-rouge">target_link_libraries</code>, <code class="language-plaintext highlighter-rouge">target_include_directories</code>, and <code class="language-plaintext highlighter-rouge">target_compile_options</code> to apply settings <em>only</em> where needed.</p>

<p>And don’t just slap <code class="language-plaintext highlighter-rouge">PUBLIC</code> everywhere. Use <code class="language-plaintext highlighter-rouge">PRIVATE</code> by default, and only expose what’s absolutely necessary. You’ll reduce build times and avoid unnecessary rebuilds.</p>

<p>And if you still need global project settings, use <code class="language-plaintext highlighter-rouge">options()</code> with good descriptions and defaults, but keep them minimal. The goal is to avoid global state as much as possible.</p>

<h2 id="4-writing-non-portable-build-instructions">4. Writing non-portable build instructions</h2>

<p>It’s easy to hack together something that works on your machine and CMake’s possibility to call external commands and scripts is very powerful. But that doesn’t mean it’s portable. Using platform-specific paths, scripts, or flags ties your build to a single setup - and breaks everything elsewhere.</p>

<p>Stick to CMake’s built-in features:</p>

<ul>
  <li>Stick global compiler flags into CMake presets.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">target_compile_options</code>, <code class="language-plaintext highlighter-rouge">target_compile_definitions</code>, etc., instead of global flags.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">cmake -P</code> and <code class="language-plaintext highlighter-rouge">cmake -E</code> for scripting, not Bash or PowerShell.</li>
  <li>Use CMake’s built-in function to find things like libraries and include directories - No hardcoded paths in CMakeLists.txt!</li>
  <li>Set up toolchain files for cross-compilation.</li>
</ul>

<p>Portability isn’t just for open-source projects. It’ll save you headaches even in single-platform codebases - especially when onboarding new devs or upgrading tools.</p>

<h2 id="5-half-baked-library-setup">5. Half-baked library setup</h2>

<p>Ever try using a library only to discover it’s missing version info, has broken exports, or forces a pile of transitive includes on you?</p>

<p>That’s what happens when the setup of the library target is incomplete or sloppy.</p>

<p>Don’t just make everything <code class="language-plaintext highlighter-rouge">PUBLIC</code>. Be precise with <code class="language-plaintext highlighter-rouge">PRIVATE</code>, <code class="language-plaintext highlighter-rouge">PUBLIC</code>, and <code class="language-plaintext highlighter-rouge">INTERFACE</code>. Set symbol visibility correctly, especially for shared libs. And always include version info (<code class="language-plaintext highlighter-rouge">VERSION</code>, <code class="language-plaintext highlighter-rouge">SOVERSION</code>) for libraries - it helps tools, consumers, and future-you.</p>

<p>For a solid setup, <a href="https://softwarecraft.ch/cmake-library-setup/">here’s a line-by-line guide</a>.</p>

<h2 id="6-no-install-instructions">6. No install instructions</h2>

<p>You might think install rules don’t matter if you’re not publishing a library. But even in internal projects, a clean <code class="language-plaintext highlighter-rouge">install()</code> setup can make packaging, CI pipelines, or dev workflows way smoother.</p>

<p>It’s not hard: a few lines in your <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> go a long way. Think of it as a foundation for future packaging, or just making builds easier to deploy locally.</p>

<h2 id="7-neglecting-maintenance-of-your-cmakeliststxt">7. Neglecting maintenance of your CMakeLists.txt</h2>

<p>CMake scripts aren’t “write-once” files. They need love too.</p>

<p>Old, bloated, or inconsistent <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> files are painful to work with and a nightmare for newcomers. Regularly refactor and clean up your build scripts - just like you do with code.</p>

<p>Update to modern CMake practices. Drop old workarounds. Add comments. Keep dependencies clean. You’ll thank yourself later.</p>

<h2 id="how-to-fix-it">How to fix it</h2>

<p>If your team is grumbling about CMake, chances are you’re stuck in one - or several - of these anti-patterns. But you don’t have to fix everything overnight.</p>

<p>Which one hurts the most depends on the project and the team, but in general, the anti-patterns often go hand in hand. Modernize your CMake scripts and tackle one thing at the time and each step brings back a little bit of joy to the build process.</p>

<style>
    /* Default: Light theme → make box darker */
    .contrast-box {
        background-color: hsla(0, 0%, 94%, 0.918);
        margin-bottom: 1.5em;
        padding: 0.5em;
    }

    /* Dark theme → make box lighter */
    @media (prefers-color-scheme: dark) {

        .contrast-box {
            background-color: hsla(0, 0%, 89%, 0.055);
        }
    }
</style>

<div class="contrast-box">
    <h1><a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
            CMake Best Practices - The book</a></h1>

    <div style="display: grid; width: 100%; grid-template-columns: 25% 1fr; grid-gap: 1%; padding-bottom: 0.5em;">
        <div>

            <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
                <img src="/images/cmake-best-practices-2nd-edition.jpg" alt="Cover of the CMake Best Practices book 2nd Edition by Dominik Berner and Mustafa Kemal Gilor" style="max-width:100%;height:auto;display:block" />

            </a>

        </div>
        <div>
            CMake Best Practices: Discover proven techniques for creating and maintaining programming projects with
            CMake. Learn how to use CMake to maximum efficiency with this compendium of best practices for a lot of
            common tasks when building C++ software.
            <br />
            <br />
            <div class="order-button">
                <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">Get it from
                    Packt</a>
            </div>

        </div>
    </div>
</div>

<p>Even if you can’t make things perfect, starting with small steps can lead quickly to improvements. Just applying <code class="language-plaintext highlighter-rouge">PRIVATE</code> and <code class="language-plaintext highlighter-rouge">PUBLIC</code> correctly can go a long way, replacing hardcoded paths with <code class="language-plaintext highlighter-rouge">find_package</code> and using CMake’s built-in functions to find libraries and include directories can make the build process more robust and portable. And usually with every step you take, you will find that the build process becomes easier to understand and maintain, leading to a more enjoyable development experience for everyone involved.</p>]]></content><author><name></name></author><category term="cmake" /><summary type="html"><![CDATA[CMake is powerful, but often blamed for bad build experiences. Most of the frustration, though, comes from avoidable mistakes. Here are 7 common CMake anti-patterns that waste your time - and how to fix them.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/cmake-logo.png" /><media:content medium="image" url="https://softwarecraft.ch//images/cmake-logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Optimize your code for reduced cost of change</title><link href="https://softwarecraft.ch//reduce-cost-of-change-in-your-code/" rel="alternate" type="text/html" title="Optimize your code for reduced cost of change" /><published>2025-01-16T00:00:00+00:00</published><updated>2025-01-16T00:00:00+00:00</updated><id>https://softwarecraft.ch//reduce-cost-of-change-in-your-code</id><content type="html" xml:base="https://softwarecraft.ch//reduce-cost-of-change-in-your-code/"><![CDATA[<p><strong>Software development is an expensive business.</strong> Measured over the lifespan of a product, the cost of maintaining and changing the code over time often greatly outweighs the initial development cost. Successful software products nowadays often have lifespans measured in decades rather than years, and often they are kept under active development throughout the whole period. Evolving technology, fixing defects, adaptation to customer needs, or pressure from competition are common reasons why software needs change. In view of this, it is paramount that <em>when designing software and writing code, you should optimize for reducing future cost of change first before anything else</em>.</p>

<h2 id="what-drives-the-cost-of-change-in-software">What drives the cost of change in software?</h2>

<p>When talking about the cost of software products, companies often talk about total cost of ownership (TCO), which in simple terms means, “How much does it cost to keep the software running and up to date?” While the cost of operating software can be a significant cost factor contributing to the TCO, often the far larger cost factor is the amount of time software developers spend changing the code, improving or adding features, and fixing defects.</p>

<blockquote>
  <p>Changing and adapting software can be the major cost driver in software development, even before operational costs.</p>
</blockquote>

<p>When maintaining software over time, there is always a base layer of effort needed just to keep the software running and up to date. These are things like changes in underlying hardware, operating systems, evolution of libraries, etc. This is what I refer to as the <em>fixed cost of change</em>. This kind of housekeeping is an important investment in keeping the cost of change low, but it is often hard to predict when this needs to be done. However, usually the far larger share of the cost is the <em>controllable cost of change</em>. This is driven by the number of feature requests, defects reported, volatility of the user base, and evolving user needs. These changes can often be planned based on observing the running system and feedback received. Thus, a development team can usually exert some control over these factors through deciding when, how, or if to implement a change, or by accepting certain limitations in functionality or stability instead of fixing a defect.</p>

<p>Looking at the typical software development process, any change made on an existing codebase usually involves the following steps:</p>

<ol>
  <li><strong>Identifying the requirements of the change</strong> - Slicing requirements into small, incremental changes helps to decide whether the change is worth implementing or not.</li>
  <li><strong>Looking for the parts of the code that need to be changed</strong> - A modularized, consistently designed codebase makes finding the parts that need to be changed easier and reduces the impact of the change.</li>
  <li><strong>Changing the code and implementing the change</strong> - Good software craft and clean implementation reduce testing effort.</li>
  <li><strong>Testing and validating the changes</strong> - Good test coverage reduces the risk of regression.</li>
  <li><strong>Deploying the changes to the production environment</strong> - Frequent and easy deployment enables fast feedback and new requirements.</li>
</ol>

<figure>
  <img src="/images/cost-of-change/cost-of-change-cycle.png" alt="The circle of managing cost of change in software." onclick="toggleSize(this)" />
  <figcaption>The circle of managing cost of change in software. (Click to enlarge, click again to reduce the size)
    
  </figcaption>
</figure>

<p>Each of these steps can be a source of cost, and each of them can be optimized to reduce the cost of change, which usually impacts the next step again. Generally, the faster a change can be implemented, tested, and deployed, the faster the loop can be closed and the faster value is generated. This includes also the waiting time between the steps. Nothing costs more than a finished change that is lying around for weeks or months before it is deployed and validated.</p>

<h2 id="controlling-the-fixed-cost-of-change">Controlling the fixed cost of change</h2>

<p>One of the most frequent shortcomings regarding high cost of change that I see is not investing in the fixed cost of change frequently. <em>Keeping your software reasonably up to date and doing so in frequent, but small increments is one of the major investments in keeping the cost of change small</em>. A frequent mistake of software teams is not keeping the software stack current and up to date, especially once the speed of feature development slows down and once the software is considered mature. The effect is then that even if a small change is needed, the cost of change is high because the first thing a developer has to do is update the software stack to a version that is still supported and maintained. While this is not only a time-consuming factor, it is often a high risk to introduce hidden regression bugs into the software, leading to a much higher validation and testing cost than what would be needed just to test the new feature.</p>

<blockquote>
  <p>Keeping your software stack up to date is not glamorous work, but it is one of the most important investments in keeping the cost of change low.</p>
</blockquote>

<p>When it comes to the controllable cost of change, there are a number of strategies that can be applied to reduce the cost of change in your code. Of the steps identifying the requirements, looking for the parts of the code that need to be changed, changing the code, testing the changes, validating the changes, and deploying the changes, all have potential for optimization. Here are some strategies that can be applied to reduce the cost of change in your code:</p>

<h3 id="identify-requirements---say-no-to-a-change">Identify requirements - Say “No” to a change</h3>

<p>Let’s start at the very beginning with <em>identifying requirements</em>. The biggest cost saver here is saying “No” to a change. One of the twelve principles of the agile manifesto is <strong>“Simplicity–the art of maximizing the amount of work not done–is essential.”</strong> The equation is simple: the fewer features and code there are in a product, the less there is to maintain. To do so, a clear discussion about the value of the change is necessary. Ask “Do we (or one of our stakeholders) benefit if we do this?” If the answer is not a clear “yes,” then don’t do it. To help with this decision, <a href="the-art-of-slicing/">rigorous slicing of stories into small, incrementally applicable changes</a> is a must. This is done to reduce development effort while maximizing the value of the change. A frequent mistake here is that teams at this point are not talking about the value, but about the predicted cost only. A typical pattern I observe is that developers are asked to estimate the change, and if they give a low enough number, the change is applied.</p>

<blockquote>
  <p>A common mistake when deciding whether to fix a defect or add a feature is to talk about cost only instead of value first.</p>
</blockquote>

<p>Another frequent omission at that stage is to not talk about what to remove from the product and talk about deprecation. If a product is running over decades, it is natural that some features at one point will become obsolete or too cumbersome to use and maintain. Having a clear strategy on how to remove or replace features from a product is as important as adding new ones. That usually involves a clear communication strategy to the users and stakeholders, a clear way to mark features as being phased out, and a clear plan on how to remove the feature from the codebase. Handling deprecation well can save a lot of cost in the long run. Removing bad or dead code reduces the noise developers have to dig through when looking for the parts of the code that need to be changed.</p>

<h3 id="finding-the-parts-of-the-code-that-need-to-be-changed">Finding the parts of the code that need to be changed</h3>

<p>Once the decision is made to implement a change, the next step is to find the parts of the code that need to be changed. This is where good software design and architecture come into play. The more modularized and decoupled your code is, the easier it is to find the parts of the code that need to be changed. There is a high chance that after a few years of development, the people maintaining the code are no longer the same people who wrote the code. And even if they are, there is a high chance that a lot of detail just got forgotten. If the code is well structured and modularized, it is much easier for new developers to find their way around the codebase and to understand what needs to be changed. Build your software around a consistent architecture and design patterns, so it is easier to find the parts of the code that need to be changed. This can start at the smallest level, for instance, on how you use functions. Are you using non-const output parameters in functions, or are you strictly using structs as return values? Do you use exceptions or error codes to signal errors? You may like or not like some of these patterns, <em>whatever you choose, be consistent</em>.</p>

<blockquote>
  <p>Consistency in software design and architecture is more important than the actual choice of the pattern, to make locating the parts of the code that need to be changed easier.</p>
</blockquote>

<p>Consistency extends all the way up to the architecture patterns like MVC, MVVM, Clean Architecture, or Hexagonal Architecture and whatever else is out there. Again, consistency is more important than the actual choice of the pattern, although choosing the completely wrong architecture can be a major cost driver. Whatever architecture you pick, favor strong modularization, low coupling, and high internal cohesion for your modules to reduce the cost of change.</p>

<p>Stick to your chosen design within a module, but decouple modules as much as possible. If your code is tightly coupled, you will have to change many other parts of the code when you change one part. This is a major cost driver in software development. The more code changed, the more likely you are to introduce bugs and the higher the testing effort will be. This, of course, has a backward effect on the decision of whether to implement the changed requirement or not. If you are sure that the change will be localized and not affect other parts of the code, it’s way easier to say “yes” to a change. Once you found the code, the fun part starts - changing the code and implementing the change.</p>

<h3 id="implementing-the-change">Implementing the change</h3>

<p>When implementing a change, having invested in good software practices upfront pays off a lot. But even if that was not done in the past, then start applying good practices with the change on hand. It might as well be that when the original code was written, there were no proper unit tests around, TDD was not practiced, and no code reviews were done. So what? Start doing that now! One of the most important things you can do at that stage is to further reduce the cost of change. I tend to say that every implementation should start with a refactoring first. Maybe the existing code is not following the current coding standards, has the wrong level of abstraction, or is not using any of the patterns you have chosen for that particular module. Don’t build on broken stilts; refactor the code first, so it is easier to understand and change.</p>

<blockquote>
  <p>Every implementation should strive to further reduce the cost of change (or at least not increase it significantly).</p>
</blockquote>

<p>There is, of course, a trade-off on how much can be done. At one point, we might have to accept that some parts of the code are just too costly to repair and that we hit a flat spot on how much we can reduce the cost of change. The trade-off here is usually whether to sacrifice internal coherence of a module but improve decoupling and isolation of “bad code” more. On a whim, I usually try to isolate first rather than to keep coherence, but that is a personal preference and depends on a lot of factors.</p>

<figure>
  <img src="/images/cost-of-change/tdd-cycle.png" alt="The TDD cycle of creating a failing test, making the test pass, refactoring, and starting again." onclick="toggleSize(this)" />
  <figcaption>The TDD cycle of creating a failing test, making the test pass, refactoring, and starting again. (Click to enlarge, click again to reduce the size)
    
  </figcaption>
</figure>

<p>A very good practice to make sure the cost of change stays manageable when implementing new features is to use a TDD approach and relentlessly apply the full cycle, which includes refactoring of the original code. To skip the last step in the TDD cycle is a direct invitation to increase the cost of change. The other benefit of a TDD approach is that test coverage of new code stays high, which helps with verifying that the change is correct and that no regression bugs are introduced.</p>

<h3 id="testing-and-validating-the-changes">Testing and validating the changes</h3>

<p>When thinking about the cost of a new feature, the testing and validation cost is often forgotten or underestimated. This often goes back to the segmentation of the system and how localized the change is. The splash radius of a change is often a very good indicator of how high the testing and validation cost will be. If the change is localized, the testing effort is usually low; the wider the effects, the more expensive testing goes. For instance, optimizing the performance of a single algorithm is usually a very localized change, and testing through a benchmark of before/after can be sufficient. Optimizing or changing a full workflow in the business logic could have a much wider effect and require a lot more testing, often involving manual testing - which is expensive.</p>

<p>While the verification of a feature - aka it works as defined - can often be automated, the validation - aka it works as expected - is often a manual process. The more manual the validation process is, usually the higher the contribution to the cost of change. It often pays off to think of what kind of validation is needed for a feature before actually implementing it. Sometimes validation of a change can only be done by the end user, so an easy way for (selectively) deploying changes to a subset of users can be a good strategy to reduce the cost of change.</p>

<h3 id="deploying-the-changes">Deploying the changes</h3>

<p>So the change is now implemented and tested! Very good, now let’s ask the developers to roll it out to the production environment. This is where the cost of change can skyrocket. If the deployment process is manual, error-prone, and time-consuming, the cost of change is high even for the tiniest change. This can go up to the point where developers avoid even the tiniest fixes for fear of the cost of deployment.</p>

<blockquote>
  <p>Invest in an easy-to-operate and automated deployment process to reduce the cost of change and deploy changes one by one instead of bundled together into large releases.</p>
</blockquote>

<p>The deployment process is often a neglected part of the software development process, but it is a very important part of the cost of change. If deployment to production is hard and involves jumping through seven hoops to get it done, a common pattern is that multiple changes are bundled together to reduce the cost of deployment. The problem here arises that at one point the feedback loop gets a lot longer and that big releases have a much higher chance to introduce hidden regression. Another pattern frequently observed is that if deployment is expensive, only big and heavy changes are even passing the decision threshold at the beginning of the cost-of-change cycle. Because, why spend days deploying a small change that might not even be needed? The result is software that might just do what it is supposed to do, but the many small kinks and annoyances that are never fixed make it a pain to use.</p>

<p>As long as a feature is not deployed, it only generates cost, but no value, so early deployment of any change will bring more value over time. This leads to one of the most destructive and behavioral patterns in software engineering. Only discussing software development from the cost perspective when evaluating whether to apply a change or not. While it might be true that software development is not cheap, holding back the work done and not letting it generate value is even more expensive. This closes the cycle of the cost of change because if features are never deployed, the cost of change for the next feature might be even higher.</p>

<h2 id="optimizing-for-reduced-cost-of-change-is-a-systemic-problem">Optimizing for reduced cost of change is a systemic problem</h2>

<p>Being aware of the cycle that drives the cost of change is the first step to reduce it. The cost of change in software engineering is not just a technical problem, but a systemic problem that involves the whole team or even larger parts of an organization.</p>

<figure>
  <img src="/images/cost-of-change/cost-of-change-cycle.png" alt="The circle of managing cost of change in software." onclick="toggleSize(this)" />
  <figcaption>The circle of managing cost of change in software. (Click to enlarge, click again to reduce the size)
    
  </figcaption>
</figure>

<p>Optimizing the cycle can be a challenging task; however, if the cycle is observed, then one can usually spot the major bottlenecks quickly. Start where it hurts the most and where work piles up because it’s waiting for action. Whether it is improving the deployment infrastructure and processes, investing in test automation, improving the architecture or design of the software, or just training to say “No” to a change, there are many ways to reduce the cost of change in software.</p>]]></content><author><name></name></author><category term="software-delivery" /><category term="agile" /><summary type="html"><![CDATA[Software products might live for a long time. After the initial cost, the cost of change is the major cost driver. Optimizing your code for reduced cost of change is paramount.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/cost-of-change/thumbnail-cost-of-change.jpg" /><media:content medium="image" url="https://softwarecraft.ch//images/cost-of-change/thumbnail-cost-of-change.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Using Conan as a CMake Dependency Provider</title><link href="https://softwarecraft.ch//conan-as-cmake-dependency-provider/" rel="alternate" type="text/html" title="Using Conan as a CMake Dependency Provider" /><published>2024-05-24T00:00:00+00:00</published><updated>2024-05-24T00:00:00+00:00</updated><id>https://softwarecraft.ch//conan-as-cmake-dependency-provider</id><content type="html" xml:base="https://softwarecraft.ch//conan-as-cmake-dependency-provider/"><![CDATA[<p><strong>Managing dependencies in CMake is hard.</strong> It’s a common pain point for C++ developers, especially when working on multi-platform projects or with complex dependencies. The introduction of dependency providers in CMake 3.24 aims to simplify this process by allowing package managers like <a href="https://conan.io/">Conan</a> to provide dependency information directly to <a href="https://cmake.org/">CMake</a>. Conan and CMake are already a powerful combination for managing C++ dependencies, and this new feature further enhances their integration. In this post, we’ll explore how to use Conan as a CMake dependency provider, making dependency management in CMake projects more seamless and efficient. <a href="https://github.com/bernedom/CMake-Conan-Dependency-Provider-Example">A sample project can be found on my github account</a></p>

<h2 id="cmake-dependency-providers-in-a-nutshell">CMake Dependency Providers in a nutshell</h2>

<p><a href="https://cmake.org/cmake/help/latest/command/cmake_language.html#dependency-providers">Dependency providers</a> are a new feature introduced in CMake 3.24 that allows package managers to provide dependency information directly to CMake. This information includes the location of the libraries, their include directories, and other necessary information for building the project. When a dependency provider is registered in CMake each call to <code class="language-plaintext highlighter-rouge">find_package</code> or <code class="language-plaintext highlighter-rouge">FetchContent_MakeAvailable</code> will trigger the corresponding function in the dependency provider script.</p>

<style>
    /* Default: Light theme → make box darker */
    .contrast-box {
        background-color: hsla(0, 0%, 94%, 0.918);
        margin-bottom: 1.5em;
        padding: 0.5em;
    }

    /* Dark theme → make box lighter */
    @media (prefers-color-scheme: dark) {

        .contrast-box {
            background-color: hsla(0, 0%, 89%, 0.055);
        }
    }
</style>

<div class="contrast-box">
    <h1><a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
            CMake Best Practices - The book</a></h1>

    <div style="display: grid; width: 100%; grid-template-columns: 25% 1fr; grid-gap: 1%; padding-bottom: 0.5em;">
        <div>

            <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">
                <img src="/images/cmake-best-practices-2nd-edition.jpg" alt="Cover of the CMake Best Practices book 2nd Edition by Dominik Berner and Mustafa Kemal Gilor" style="max-width:100%;height:auto;display:block" />

            </a>

        </div>
        <div>
            CMake Best Practices: Discover proven techniques for creating and maintaining programming projects with
            CMake. Learn how to use CMake to maximum efficiency with this compendium of best practices for a lot of
            common tasks when building C++ software.
            <br />
            <br />
            <div class="order-button">
                <a href="https://www.packtpub.com/en-us/product/cmake-best-practices-9781835880647">Get it from
                    Packt</a>
            </div>

        </div>
    </div>
</div>

<p>Behind the curtains, a dependency provider is a CMake script that provides functions to locate dependencies through whatever means it can. In the case of Conan, this means calling calling <code class="language-plaintext highlighter-rouge">conan install</code> with the appropriate toolchain configuration and then extracting the necessary information from the files generated by Conan. To install a depenndency provider the script containing the provider definition is passed using the <code class="language-plaintext highlighter-rouge">CMAKE_PROJECT_TOP_LEVEL_INCLUDES</code> variable either from the command line or through a <a href="https://softwarecraft.ch/cmake-presets-best-practices/">CMake preset</a>
The official <a href="https://cmake.org/cmake/help/latest/command/cmake_language.html#dependency-providers">CMake documentation provides an in-depth explanation of how dependency providers work and how to create your own</a>.</p>

<h2 id="setting-up-conan-as-a-cmake-dependency-provider">Setting up Conan as a CMake Dependency Provider</h2>

<p>To set up Conan as a dependency provider we need three things:</p>

<ul>
  <li>CMake 3.24 or later</li>
  <li>Conan 2.0.2 or later</li>
  <li>The Conan CMake helper script to install it as a dependency provider</li>
</ul>

<p>Installing CMake and Conan is straightforward and well-documented, so we won’t cover it here. The Conan CMake helper script is a CMake script that sets up Conan as a dependency provider in your project can be found in the <a href="https://github.com/conan-io/cmake-conan">cmake-conan repository</a>. As of May 2024 the support is still experimental, but it should be stable enough for most use cases. To use the Conan CMake helper script, you can either download it manually or use it as a git submodule in your project. Since dependency providers have to be configured at the very beginning of the CMake configuration process, getting it over <code class="language-plaintext highlighter-rouge">FetchContent</code> or similar is not an option.</p>

<p>I usually end up with a directory structure like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── cmake-conan <span class="c"># git submodule</span>
│   ├── conan_provider.cmake
│   └── ...
├── CMakeLists.txt
├── conanfile.txt
└── src
    └── ...
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">conan_provider.cmake</code> script is the Conan CMake helper script that we will use to set up Conan as a dependency provider. The <code class="language-plaintext highlighter-rouge">conanfile.txt</code> file is the Conan configuration file that lists the dependencies of the project. The <code class="language-plaintext highlighter-rouge">src</code> directory contains the source code of the project and the <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> file contains the main CMake configuration.</p>

<h2 id="preparing-the-conan-and-cmake-configuration">Preparing the Conan and CMake configuration</h2>

<p>Let’s assume we are building a simple project that depends on the <a href="https://github.com/fmtlib/fmt">fmt library</a> for string formatting, which is available from the <a href="https://conan.io/center/recipes/fmt">conan center repository</a>. One of the nice things about dependency providers is that the <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> does not need any special configuration and can just use <code class="language-plaintext highlighter-rouge">find_package</code> and <code class="language-plaintext highlighter-rouge">target_link_libraries</code> as usual. The Conan CMake helper script will take care of the rest. The <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> file for the project looks like this:</p>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cmake_minimum_required</span><span class="p">(</span>VERSION 3.24<span class="p">)</span>

<span class="nb">project</span><span class="p">(</span>
    hello_world
    LANGUAGES CXX
<span class="p">)</span>

<span class="nb">find_package</span><span class="p">(</span>fmt 10.2.1 REQUIRED<span class="p">)</span>
<span class="nb">add_executable</span><span class="p">(</span>hello src/main.cpp<span class="p">)</span>
<span class="nb">target_link_libraries</span><span class="p">(</span>
    hello
    PRIVATE fmt::fmt
<span class="p">)</span>
</code></pre></div></div>

<p>Next, The <code class="language-plaintext highlighter-rouge">conanfile.txt</code> file lists the dependencies of the project:</p>

<pre><code class="language-txt">[requires]
fmt/10.2.1

[generators]
CMakeDeps
</code></pre>

<p>Using the <code class="language-plaintext highlighter-rouge">CMakeDeps</code> generator in the <code class="language-plaintext highlighter-rouge">conanfile.txt</code> file tells Conan to generate the necessary information for CMake’s find_package function. This is necessary for the Conan CMake helper script to work correctly. There is also the <code class="language-plaintext highlighter-rouge">CMakeToolchain</code> generator that generates a toolchain file for CMake, but this is not recommended when using Conan as a dependency provider.</p>

<p>With that in place, we can now set up Conan as a dependency provider in the <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> file:</p>

<h2 id="using-conan-as-a-cmake-dependency-provider">Using Conan as a CMake Dependency Provider</h2>

<p>To set up Conan as a dependency provider, we need to pass the <code class="language-plaintext highlighter-rouge">conan_provider.cmake</code> script to CMake using the <code class="language-plaintext highlighter-rouge">CMAKE_PROJECT_TOP_LEVEL_INCLUDES</code> variable. This can be done either from the command line or through a CMake preset. The <code class="language-plaintext highlighter-rouge">conan_provider.cmake</code> script is the Conan CMake helper script that sets up Conan as a dependency provider.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmake -S . -B build -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=./cmake-conan/conan_provider.cmake -DCMAKE_BUILD_TYPE=Debug
</code></pre></div></div>

<p>Note that Conan requires the CMAKE_BUILD_TYPE to be set in order to download or build the correct version of the dependencies. The <code class="language-plaintext highlighter-rouge">conan_provider.cmake</code> script will take care of setting up the necessary Conan profiles and installing the dependencies in the local cache. The binaries and include files for the dependencies will be placed in the local Conan cache, which is usually located in the user’s home directory. Only the configuration files generated by Conan will be placed in the build directory. By default the Conan helper script is configured to use the <code class="language-plaintext highlighter-rouge">default</code> Conan profile and it tries to build any missing dependencies from source. These settings can be changed by setting the <code class="language-plaintext highlighter-rouge">CONAN_BUILD_PROFILE</code> and <code class="language-plaintext highlighter-rouge">CONAN_INSTALL_ARGS</code> variables respectively.</p>

<p>After running CMake, the project can be built as usual:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmake --build build
</code></pre></div></div>

<p>And with that the project the project is set up to use Conan as a dependency provider. 
I recommend to use CMake presets to make the configuration more reproducible and to avoid having to remember the command line arguments, especially when working with multiple configurations or platforms. For public projects I generally include the Conan helper script as a git submodule but depending on the project it might be easier to just download it manually.</p>

<h2 id="is-it-really-that-easy">Is it really that easy?</h2>

<p>The introduction of CMakes dependency providers closes a long-existing gap in CMake’s dependency management and like CMake Presets I consider them a vast improvement regarding the tooling situation around C++ projects. So far I have only used Conan as a dependency provider but I am looking forward to seeing how other package managers will integrate with CMake. What I particularly like is that the CMake configuration can remain simple and platform agnostic while the complex dependency management is handled by Conan. This makes it easier to switch between different build systems or to integrate the project into a larger build system and in my opinion is a great incentive to use Conan for C++ projects.</p>]]></content><author><name></name></author><category term="cmake" /><category term="conan" /><summary type="html"><![CDATA[With the addition of dependency providers in CMake 3.24 using Conan to manage dependencies becomes easier and more integrated. This post shows a step-by-step guide on how to use Conan as a CMake dependency provider.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/cmake-conan-logo.png" /><media:content medium="image" url="https://softwarecraft.ch//images/cmake-conan-logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>