<?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-10-01T13:25:56+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">C++, CMake, Conan and a Play Store: Building a Multi-ABI Qt6 Android App Bundle</title><link href="https://softwarecraft.ch//publishing-android-aab-qt6-and-cmake/" rel="alternate" type="text/html" title="C++, CMake, Conan and a Play Store: Building a Multi-ABI Qt6 Android App Bundle" /><published>2026-09-16T00:00:00+00:00</published><updated>2026-09-16T00:00:00+00:00</updated><id>https://softwarecraft.ch//publishing-android-aab-qt6-and-cmake</id><content type="html" xml:base="https://softwarecraft.ch//publishing-android-aab-qt6-and-cmake/"><![CDATA[<p><strong>C++ and Qt are a powerful combination for cross-platform development, but building for Android introduces unique challenges.</strong> While for a long time it was enough to just build the software and <a href="/cmake-android-apk-and-qt/">wrap it into an apk</a>, modern Android development often requires handling multiple ABIs, integrating with Java tooling, and managing dependencies in a cross-compiled environment. This sounds tough, but by combining <a href="https://cmake.org/">CMake</a>, <a href="https://conan.io/">Conan</a>, and <a href="https://www.qt.io/">Qt6</a>’s cross-build capabilities, it becomes manageable.</p>

<p>This article is based on the <a href="https://github.com/bernedom/QRLite">QRLite</a>-Project. QRLite is a small, local-only and ad-free app for reading QR-Codes that runs on Desktop and Android. It is a C++/Qt6 Android app, using <a href="/cmake-presets-best-practices/">CMake presets</a>, <a href="/conan-as-cmake-dependency-provider/">Conan 2 as a CMake dependency provider</a>, and Qt6’s own <code class="language-plaintext highlighter-rouge">qt-cmake</code> tooling to go from source to a single installable <code class="language-plaintext highlighter-rouge">.aab</code> (Android App Bundle) covering arm and x86 architectures.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>Let’s see how it all comes together.</p>

<h2 id="the-high-level-build-process-for-multi-abi-android-app-bundles">The high level build process for multi-ABI Android App Bundles</h2>

<p><a href="/cmake-android-apk-and-qt/">The basic setup for a Qt/CMake Android build is covered in a previous post</a>. That post covers NDK/SDK installation, environment variables, basic <code class="language-plaintext highlighter-rouge">CMakePresets.json</code> anatomy, and <code class="language-plaintext highlighter-rouge">adb install</code>. All are still valid, and we
won’t repeat it here. However this time, we have some dependencies to manage, and this
post picks up from there and adds Conan 2 + Qt6, then we go all the way to
creating a multi-ABI bundle.</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>Before we dive into the details of Conan and Qt integration, let’s outline the high-level steps involved in building a multi-ABI Android App Bundle with CMake. At the baseline, an aab file is essentially a collection of packages for different plattforms, so the process of building one with C++ involves:</p>

<ul>
  <li>Configuring CMake for each target ABI.</li>
  <li>Fetching and resolving dependencies for each ABI.</li>
  <li>Building the project for each ABI.</li>
  <li>Collecting the resulting binaries and resources.</li>
  <li>Packaging them into a single <code class="language-plaintext highlighter-rouge">.aab</code> file.</li>
</ul>

<p>While there exist the <code class="language-plaintext highlighter-rouge">qt-cmake</code> wrapper to simplify Qt-specific configuration for Android, the underlying challenge of building for multiple ABIs remains. Each ABI still requires a separate CMake configuration and build step. Also qt-cmake has a few quirks and additional steps compared to plain CMake and if your stack is not Qt-centric, plain CMake is often easier to work with. So the approach here is to use plain CMake for building and testing and only use <code class="language-plaintext highlighter-rouge">qt-cmake</code> when necessary for packaging the final <code class="language-plaintext highlighter-rouge">.aab</code>.
Let’s walk through each of these steps in more detail.</p>

<h2 id="the-dependency-problem-conan-as-a-cmake-dependency-provider">The dependency problem: Conan as a CMake dependency provider</h2>

<p>QRLite depends on <a href="https://github.com/zxing-cpp/zxing-cpp">zxing-cpp</a> for QR
decoding and <a href="https://github.com/catchorg/Catch2">Catch2</a> for its test suite, which will not be bundled into the final Android App Bundle.</p>

<p>To manage these dependencies across multiple ABIs, we need a tool that understands both CMake and cross-compilation.
Conan solves the “build the right binary for the right ABI” half of that
problem. CMake’s <strong>dependency provider</strong> mechanism is what lets Conan do it
<em>transparently</em>, from inside <code class="language-plaintext highlighter-rouge">find_package()</code>, instead of requiring every
consumer of the project to run a separate <code class="language-plaintext highlighter-rouge">conan install</code> step by hand and
remember to keep it in sync with the CMake configure. Since CMake 3.24,
setting</p>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">set</span><span class="p">(</span>CMAKE_PROJECT_TOP_LEVEL_INCLUDES <span class="s2">"path/to/conan_provider.cmake"</span><span class="p">)</span>
</code></pre></div></div>

<p>before the first <code class="language-plaintext highlighter-rouge">project()</code> call registers a script that CMake consults
whenever <code class="language-plaintext highlighter-rouge">find_package()</code> can’t resolve a package the normal way. The script
is <a href="https://github.com/conan-io/cmake-conan"><code class="language-plaintext highlighter-rouge">cmake-conan</code></a>, which QRLite
vendors as a git submodule at <code class="language-plaintext highlighter-rouge">cmake-conan/</code>. In <a href="https://github.com/bernedom/QRLite/blob/main/CMakePresets.json"><code class="language-plaintext highlighter-rouge">CMakePresets.json</code></a> this
becomes a small hidden preset that every other configuration inherits from:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"conan-dependency-provider"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hidden"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"cacheVariables"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"CMAKE_PROJECT_TOP_LEVEL_INCLUDES"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${sourceDir}/cmake-conan/conan_provider.cmake"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>With that in place, the provider intercepts <code class="language-plaintext highlighter-rouge">find_package(ZXing REQUIRED)</code>
and <code class="language-plaintext highlighter-rouge">find_package(Catch2)</code> and Conan runs automatically: it reads
<code class="language-plaintext highlighter-rouge">conanfile.txt</code>, resolves and (if needed) builds the dependency graph for the
active profile, and generates the <code class="language-plaintext highlighter-rouge">CMakeDeps</code> config files that
<code class="language-plaintext highlighter-rouge">find_package()</code> then picks up as if they’d always been there. QRLite’s
<code class="language-plaintext highlighter-rouge">conanfile.txt</code> is intentionally minimal:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[requires]</span><span class="w">
</span><span class="na">catch2/3.10.0</span><span class="w">
</span><span class="na">zxing-cpp/2.3.0</span><span class="w">

</span><span class="nn">[generators]</span><span class="w">
</span><span class="na">CMakeDeps</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">CMakeDeps</code> is the generator that matters here — it’s what produces the
<code class="language-plaintext highlighter-rouge">Find&lt;Package&gt;.cmake</code>/<code class="language-plaintext highlighter-rouge">&lt;package&gt;-config.cmake</code> files that make Conan-provided
dependencies look like any other CMake package to <code class="language-plaintext highlighter-rouge">find_package()</code>. For a more in-depth explanation, see the <a href="/conan-as-cmake-dependency-provider/">Article @Conan as CMake Dependency Provider”</a>.</p>

<h3 id="host-and-build-profiles">Host and build profiles</h3>

<p>Once you’re cross-compiling, “what compiler and settings should this
dependency be built with” stops having one answer, Conan needs to know about
<em>two</em> toolchains at once:</p>

<ul>
  <li>the <strong>build profile</strong>: the machine actually running the compiler (your
desktop/CI container, x86_64 Linux, system GCC/Clang)</li>
  <li>the <strong>host profile</strong>: the machine the binary will <em>run on</em> (an Android
device — a given ABI, the NDK’s Clang, <code class="language-plaintext highlighter-rouge">libc++</code>, a minimum API level)</li>
</ul>

<p>For a desktop build these happen to be the same, so Conan defaults quietly
work. For Android they diverge on almost every axis: <code class="language-plaintext highlighter-rouge">arch</code>, <code class="language-plaintext highlighter-rouge">os</code>,
<code class="language-plaintext highlighter-rouge">compiler</code>, <code class="language-plaintext highlighter-rouge">compiler.libcxx</code>, even <code class="language-plaintext highlighter-rouge">compiler.version</code> (the NDK ships its own
Clang, not whatever <code class="language-plaintext highlighter-rouge">clang</code>). <a href="/using-devcontainers-with-cpp/">QRLite’s
devcontainer</a> and CI image (<code class="language-plaintext highlighter-rouge">bernedom/qtandroidbuilder</code>) ship pre-built Conan
profiles for exactly this split, one host profile per Android ABI/build type
(<code class="language-plaintext highlighter-rouge">android-debug</code>, <code class="language-plaintext highlighter-rouge">android-release</code>) and a build profile for the container
itself (<code class="language-plaintext highlighter-rouge">android</code>). A host profile looks roughly like:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[settings]</span><span class="w">
</span><span class="py">arch</span><span class="p">=</span><span class="s">armv8</span>
<span class="py">os</span><span class="p">=</span><span class="s">Android</span>
<span class="py">os.api_level</span><span class="p">=</span><span class="s">31</span>
<span class="py">compiler</span><span class="p">=</span><span class="s">clang</span>
<span class="py">compiler.version</span><span class="p">=</span><span class="s">18</span>
<span class="py">compiler.libcxx</span><span class="p">=</span><span class="s">c++_shared</span>
<span class="py">compiler.cppstd</span><span class="p">=</span><span class="s">gnu17</span>
<span class="py">build_type</span><span class="p">=</span><span class="s">Release</span>
<span class="w">
</span><span class="nn">[conf]</span><span class="w">
</span><span class="na">tools.android:</span><span class="py">ndk_path</span><span class="p">=</span><span class="s">$ANDROID_NDK_HOME</span>
</code></pre></div></div>

<p>with a separate one per ABI (<code class="language-plaintext highlighter-rouge">armv7</code>, <code class="language-plaintext highlighter-rouge">armv8</code>, <code class="language-plaintext highlighter-rouge">x86_64</code> map to
<code class="language-plaintext highlighter-rouge">armeabi-v7a</code>/<code class="language-plaintext highlighter-rouge">arm64-v8a</code>/<code class="language-plaintext highlighter-rouge">x86_64</code>). The desktop build profile is the
“normal” one, plain <code class="language-plaintext highlighter-rouge">os=Linux</code>, system C++-Compiler. CMakePresets.json wires the pair
in as cache variables that <code class="language-plaintext highlighter-rouge">cmake-conan</code> reads directly:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"conan-android-debug"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hidden"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"cacheVariables"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"CONAN_HOST_PROFILE"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android-debug"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"CONAN_BUILD_PROFILE"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="making-find_package-work-for-conan--qt6-while-cross-compiling">Making <code class="language-plaintext highlighter-rouge">find_package</code> work for Conan + Qt6 while cross-compiling</h2>

<p>Getting Conan to <em>produce</em> the right binaries is half the battle; getting
CMake’s <code class="language-plaintext highlighter-rouge">find_package()</code> to actually <em>find</em> them while an Android toolchain
file is active is the other half. The NDK’s CMake toolchain file is
deliberately strict about this — by default it sets
<code class="language-plaintext highlighter-rouge">CMAKE_FIND_ROOT_PATH_MODE_PACKAGE</code>/<code class="language-plaintext highlighter-rouge">LIBRARY</code>/<code class="language-plaintext highlighter-rouge">INCLUDE</code> to <code class="language-plaintext highlighter-rouge">ONLY</code>, meaning
<code class="language-plaintext highlighter-rouge">find_package()</code> will only look inside <code class="language-plaintext highlighter-rouge">CMAKE_FIND_ROOT_PATH</code> (the sysroot).
That’s correct behaviour for system libraries, but it also means CMake will
silently refuse to see Conan’s host-profile packages, which live outside the
NDK sysroot entirely. QRLite’s <code class="language-plaintext highlighter-rouge">android-ndk</code> hidden preset relaxes exactly
this:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"cacheVariables"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ANDROID_PLATFORM"</span><span class="p">:</span><span class="w"> </span><span class="s2">"31"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ANDROID_SDK_ROOT"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/opt/android"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"CMAKE_FIND_ROOT_PATH_MODE_PACKAGE"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BOTH"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"CMAKE_FIND_ROOT_PATH_MODE_LIBRARY"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BOTH"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"CMAKE_FIND_ROOT_PATH_MODE_INCLUDE"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BOTH"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">BOTH</code> tells CMake to search the sysroot <em>and</em> the ordinary
<code class="language-plaintext highlighter-rouge">CMAKE_PREFIX_PATH</code>/<code class="language-plaintext highlighter-rouge">CMAKE_MODULE_PATH</code>, which is where both Conan’s
generated package files and Qt6’s Android package live.</p>

<p>Qt6 itself adds a second wrinkle: unlike Qt5’s single <code class="language-plaintext highlighter-rouge">CMAKE_PREFIX_PATH</code>
from the prior article, Qt6 for Android ships <strong>one full install per ABI</strong>
(<code class="language-plaintext highlighter-rouge">android_armv7/</code>, <code class="language-plaintext highlighter-rouge">android_arm64_v8a/</code>, <code class="language-plaintext highlighter-rouge">android_x86_64/</code>), each exposed
through a <code class="language-plaintext highlighter-rouge">QT_PATH_ANDROID_ABI_&lt;abi&gt;</code> variable when you’re building for
multiple ABIs at once. QRLite’s top-level <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> combines Conan’s per-ABI output directory and Qt6’s per-ABI install into one manual prefix-path prepend, keyed on <code class="language-plaintext highlighter-rouge">CMAKE_ANDROID_ARCH_ABI</code>:</p>

<div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">if</span><span class="p">(</span>ANDROID<span class="p">)</span>
  <span class="nb">set</span><span class="p">(</span>CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH<span class="p">)</span>
  <span class="nb">set</span><span class="p">(</span>CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH<span class="p">)</span>

  <span class="nb">if</span><span class="p">(</span>CMAKE_ANDROID_ARCH_ABI STREQUAL <span class="s2">"armeabi-v7a"</span><span class="p">)</span>
    <span class="nb">list</span><span class="p">(</span>PREPEND CMAKE_PREFIX_PATH <span class="s2">"</span><span class="si">${</span><span class="nv">QT_PATH_ANDROID_ABI_armeabi-v7a</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="nb">list</span><span class="p">(</span>PREPEND CMAKE_PREFIX_PATH <span class="s2">"</span><span class="si">${</span><span class="nv">CONAN_ARMEABI_V7A_DIR</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="nb">list</span><span class="p">(</span>PREPEND CMAKE_MODULE_PATH <span class="s2">"</span><span class="si">${</span><span class="nv">CONAN_ARMEABI_V7A_DIR</span><span class="si">}</span><span class="s2">/cmake"</span><span class="p">)</span>
  <span class="nb">elseif</span><span class="p">(</span>CMAKE_ANDROID_ARCH_ABI STREQUAL <span class="s2">"arm64-v8a"</span><span class="p">)</span>
    <span class="nb">list</span><span class="p">(</span>PREPEND CMAKE_PREFIX_PATH <span class="s2">"</span><span class="si">${</span><span class="nv">QT_PATH_ANDROID_ABI_arm64-v8a</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="nb">list</span><span class="p">(</span>PREPEND CMAKE_PREFIX_PATH <span class="s2">"</span><span class="si">${</span><span class="nv">CONAN_ARM64_DIR</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="nb">list</span><span class="p">(</span>PREPEND CMAKE_MODULE_PATH <span class="s2">"</span><span class="si">${</span><span class="nv">CONAN_ARM64_DIR</span><span class="si">}</span><span class="s2">/cmake"</span><span class="p">)</span>
  <span class="nb">elseif</span><span class="p">(</span>CMAKE_ANDROID_ARCH_ABI STREQUAL <span class="s2">"x86_64"</span><span class="p">)</span>
    <span class="nb">list</span><span class="p">(</span>PREPEND CMAKE_PREFIX_PATH <span class="s2">"</span><span class="si">${</span><span class="nv">QT_PATH_ANDROID_ABI_x86_64</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="nb">list</span><span class="p">(</span>PREPEND CMAKE_PREFIX_PATH <span class="s2">"</span><span class="si">${</span><span class="nv">CONAN_X86_64_DIR</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="nb">list</span><span class="p">(</span>PREPEND CMAKE_MODULE_PATH <span class="s2">"</span><span class="si">${</span><span class="nv">CONAN_X86_64_DIR</span><span class="si">}</span><span class="s2">/cmake"</span><span class="p">)</span>
  <span class="nb">endif</span><span class="p">()</span>
<span class="nb">endif</span><span class="p">()</span>

<span class="nb">find_package</span><span class="p">(</span>Qt6 COMPONENTS Core Gui Multimedia Qml Quick REQUIRED<span class="p">)</span>
<span class="nb">find_package</span><span class="p">(</span>ZXing REQUIRED<span class="p">)</span>
</code></pre></div></div>

<p>This is a bit of a workaround to make multi-ABI builds work with Conan and Qt and it would be nicer, if this would be covered by one of the presets, but so far this did not work out as we see later. This hack exists because a single CMake <em>configure</em> only
ever targets one ABI, but when creating the <em>bundle</em>, <code class="language-plaintext highlighter-rouge">qt-cmake</code> re-invokes
CMake three times against the same source tree from a fourth, top-level
configure, and that fourth configure needs to know where each of the three
per-ABI Conan installs ended up. <code class="language-plaintext highlighter-rouge">CONAN_ARM64_DIR</code> and friends are just plain
cache that are passed to the dependency provider and ultimately used by the bundle preset to locate each ABI’s Conan install.</p>

<h2 id="from-one-apk-to-one-bundle-why-three-abis-means-three-builds">From one APK to one bundle: why three ABIs means three builds</h2>

<p>If you’re building an <code class="language-plaintext highlighter-rouge">.apk</code> you <code class="language-plaintext highlighter-rouge">adb install</code> yourself can happily target one ABI. What the Play Store wants is different: a single <code class="language-plaintext highlighter-rouge">.aab</code> that contains the app’s shared
libraries for <em>every</em> ABI you support, with Google Play generating
per-device APKs from it at install time. While CMake is great for cross-compiling it cannot build for more than one ABI at a time. A shortcoming that regularly sparks dicussions in the community.</p>

<p>And unfortunately, the android configuration such as <code class="language-plaintext highlighter-rouge">ANDROID_ABI</code> are configure-time settings. So the only way to get three ABIs’ worth of native libraries
is to run three independent configure+build cycles — which is exactly what
QRLite’s per-ABI presets (<code class="language-plaintext highlighter-rouge">android-armeabi-v7a</code>, <code class="language-plaintext highlighter-rouge">android-arm64-v8a</code>,
<code class="language-plaintext highlighter-rouge">android-x86_64</code>) are for, each with its own <code class="language-plaintext highlighter-rouge">binaryDir</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmake <span class="nt">--preset</span> ci-ninja-android-armeabi-v7a-debug
cmake <span class="nt">--preset</span> ci-ninja-android-arm64-v8a-debug
cmake <span class="nt">--preset</span> ci-ninja-android-x86_64-debug

cmake <span class="nt">--build</span> build_android_armeabi-v7a
cmake <span class="nt">--build</span> build_android_arm64-v8a
cmake <span class="nt">--build</span> build_android_x86_64
</code></pre></div></div>

<p>At this point you have three separate <code class="language-plaintext highlighter-rouge">build_android_&lt;abi&gt;/</code> trees, each
with its own <code class="language-plaintext highlighter-rouge">.so</code>, and each with its own Conan install directory
(<code class="language-plaintext highlighter-rouge">build_android_&lt;abi&gt;/conan/</code>) built against that ABI’s host profile. Three
APKs’ worth of native code, but Play Store bundles aren’t “three APKs
concatenated,” they’re one Gradle-shaped Android project with all three
<code class="language-plaintext highlighter-rouge">.so</code> sets dropped into the right per-ABI <code class="language-plaintext highlighter-rouge">jniLibs</code> folders. Producing that
is a fourth, different kind of build.</p>

<h2 id="tying-it-together-the-bundle-build">Tying it together: the bundle build</h2>

<p>Qt6 has first-class support for aab of build, but it’s driven through
<code class="language-plaintext highlighter-rouge">qt-cmake</code> rather than plain <code class="language-plaintext highlighter-rouge">cmake</code>, and it needs
to know where all three of the per-ABI pieces (Qt installs <em>and</em> Conan
installs) actually live, since this configure isn’t cross-compiling anything
itself; it’s orchestrating the three that already ran. That’s the
<code class="language-plaintext highlighter-rouge">android-all-archs</code> preset:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"android-all-archs"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hidden"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"inherits"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"android-ndk"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"cacheVariables"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"QT_ANDROID_ABIS"</span><span class="p">:</span><span class="w"> </span><span class="s2">"x86_64;arm64-v8a;armeabi-v7a"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"QT_ANDROID_BUILD_ALL_ABIS"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OFF"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"binaryDir"</span><span class="p">:</span><span class="w"> </span><span class="s2">"build_android_bundle"</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Qt-android-all-archs"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hidden"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"cacheVariables"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"QT_CHAINLOAD_TOOLCHAIN_FILE"</span><span class="p">:</span><span class="w"> </span><span class="s2">"$env{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"CONAN_ARM64_DIR"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${sourceDir}/build_android_arm64-v8a/conan"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"CONAN_ARMEABI_V7A_DIR"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${sourceDir}/build_android_armeabi-v7a/conan"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"CONAN_X86_64_DIR"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${sourceDir}/build_android_x86_64/conan"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"QT_PATH_ANDROID_ABI_armeabi-v7a"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/opt/Qt/6.11.1/android_armv7/"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"QT_PATH_ANDROID_ABI_arm64-v8a"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/opt/Qt/6.11.1/android_arm64_v8a/"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"QT_PATH_ANDROID_ABI_x86_64"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/opt/Qt/6.11.1/android_x86_64/"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"QT_HOST_PATH"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/opt/Qt/6.11.1/gcc_64/"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"environment"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"QT_HOST_PATH"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/opt/Qt/6.11.1/gcc_64/"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This is the missing piece from the “hack” in the top-level <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code>
earlier: <code class="language-plaintext highlighter-rouge">CONAN_ARM64_DIR</code>, <code class="language-plaintext highlighter-rouge">CONAN_ARMEABI_V7A_DIR</code>, and <code class="language-plaintext highlighter-rouge">CONAN_X86_64_DIR</code>
are set here explicitly, pointing back at the three builds that already ran.
<code class="language-plaintext highlighter-rouge">QT_ANDROID_BUILD_ALL_ABIS=OFF</code> combined with an explicit <code class="language-plaintext highlighter-rouge">QT_ANDROID_ABIS</code>
list tells Qt6 “don’t try to build these yourself, I’ve already built them,
just assemble the bundle from what’s on disk.” Two details matter for
actually running this configure step:</p>

<ul>
  <li>It has to be invoked with <strong><code class="language-plaintext highlighter-rouge">qt-cmake</code> from one of the Android Qt
installs</strong>, not plain <code class="language-plaintext highlighter-rouge">cmake</code>. <code class="language-plaintext highlighter-rouge">qt-cmake</code> is what wires in Qt6’s Android
packaging machinery (the <code class="language-plaintext highlighter-rouge">aab</code> target itself doesn’t exist otherwise).</li>
  <li><code class="language-plaintext highlighter-rouge">QT_ANDROID_MULTI_ABI_FORWARD_VARS</code> needs to include <code class="language-plaintext highlighter-rouge">ANDROID_PLATFORM</code> so
the per-ABI Android platform level set in the <code class="language-plaintext highlighter-rouge">android-ndk</code> preset actually
propagates into the bundle project.</li>
</ul>

<p>Put together, the full sequence looks like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmake <span class="nt">--preset</span> ci-ninja-android-armeabi-v7a-debug
cmake <span class="nt">--preset</span> ci-ninja-android-arm64-v8a-debug
cmake <span class="nt">--preset</span> ci-ninja-android-x86_64-debug

cmake <span class="nt">--build</span> ./build_android_armeabi-v7a
cmake <span class="nt">--build</span> ./build_android_arm64-v8a
cmake <span class="nt">--build</span> ./build_android_x86_64

/opt/Qt/6.11.1/android_x86_64/bin/qt-cmake <span class="se">\</span>
  <span class="nt">--preset</span> ci-ninja-android-all-archs-debug <span class="se">\</span>
  <span class="nt">-DQT_ANDROID_MULTI_ABI_FORWARD_VARS</span><span class="o">=</span><span class="s2">"ANDROID_PLATFORM"</span>

cmake <span class="nt">--build</span> ./build_android_bundle/ <span class="nt">--target</span> aab
</code></pre></div></div>

<p>That last build produces
<code class="language-plaintext highlighter-rouge">build_android_bundle/QRLiteApp/android-build/build/outputs/bundle/debug/android-build-debug.aab</code>, a single file containing native code for all three ABIs, ready for
signing.</p>

<p>One more thing worth flagging if you’re following along with your own
project: QRLiteApp’s <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> sets
<code class="language-plaintext highlighter-rouge">QT_ANDROID_TARGET_SDK_VERSION 36</code>. Most Qt/Android tutorials and Stack
Overflow answers you’ll find still assume an SDK version several years old, but Google’s Play Store minimum target API requirement moves roughly once a
year, and Qt’s own defaults lag behind it. Worth checking you’re not
building against a target SDK version Play Store will reject before you get
to the upload step.</p>

<h2 id="what-we-have-and-whats-next">What we have, and what’s next</h2>

<p>So at this point, we have Conan 2 resolving <code class="language-plaintext highlighter-rouge">zxing-cpp</code> and <code class="language-plaintext highlighter-rouge">Catch2</code> per-ABI
through a CMake dependency provider, <code class="language-plaintext highlighter-rouge">find_package()</code> correctly seeing both
Conan and Qt6 across an Android cross-compile, and a <code class="language-plaintext highlighter-rouge">qt-cmake</code>-driven bundle
step that turns three independent ABI builds into one <code class="language-plaintext highlighter-rouge">.aab</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="/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>At this point you can sign the <code class="language-plaintext highlighter-rouge">.aab</code> and upload it to the Google Play Store (or any derivative app store) for distribution. While creating an <code class="language-plaintext highlighter-rouge">.aab</code> is a step up in complexity compared to a single-ABI APK, it simplifies distribution and ensures that users get the best possible version of your app for their device, so it is generally worth the extra effort.</p>

<hr />
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>The full, working set of <code class="language-plaintext highlighter-rouge">CMakeLists.txt</code> files, <code class="language-plaintext highlighter-rouge">CMakePresets.json</code>, the <code class="language-plaintext highlighter-rouge">conanfile.txt</code>, and a ready-made devcontainer are all in the <a href="https://github.com/bernedom/QRLite">QRLite repository</a>. QRLite is currently available as a closed beta on the Google Play Store. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Dominik Berner</name></author><category term="android" /><category term="qt6" /><category term="cmake" /><summary type="html"><![CDATA[How to use C++, CMake, and Conan as a CMake dependency provider along with a three-way Qt6 cross-build to produce a single Android App Bundle from one CMake project.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/qml_on_android/cmake-android-qt-logo.png" /><media:content medium="image" url="https://softwarecraft.ch//images/qml_on_android/cmake-android-qt-logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Agentic Coding is Brute-Force Software Engineering</title><link href="https://softwarecraft.ch//agentic-coding-is-brute-force-software-engineering/" rel="alternate" type="text/html" title="Agentic Coding is Brute-Force Software Engineering" /><published>2026-07-09T00:00:00+00:00</published><updated>2026-07-09T00:00:00+00:00</updated><id>https://softwarecraft.ch//agentic-coding-is-brute-force-software-engineering</id><content type="html" xml:base="https://softwarecraft.ch//agentic-coding-is-brute-force-software-engineering/"><![CDATA[<p><strong>Writing code faster is not the same as delivering better software.</strong>  There is an inconvenient truth about agentic and AI-assisted coding: it is brute-force software engineering. It can help you write code faster, but it does not help you deliver better software faster. Working with AI-assisted coding is a productivity boost. Features get implemented in mere hours instead of days and weeks, each iteration on the software becomes cheaper and faster. This sounds like the holy grail of software development, but is it?</p>

<h2 id="good-software-design-is-a-survival-trait">Good software design is a survival trait</h2>

<p>Before the massive adoption of AI-assisted coding, producing running software was expensive and time-consuming, not just in money, but in focus, time and cognitive energy. This scarcity forced software engineers to think about the design of their software. A badly written system meant weeks of lost work and frustration. A wrongly chosen data structure could jeopardize the performance of an entire system and a rewrite of it could take months.</p>

<p>This is why practices such as domain driven design, test-driven development, and code reviews were invented and popularized. They were not academic exercises, but survival traits.</p>

<blockquote>
  <p>If implementing is expensive, you have to think before you implement.</p>
</blockquote>

<p>An architect spending days thinking about how data is modeled doesn’t do this out of nostalgia. She does it because mistakes there could cost months of work later. This economic pressure is what formed modern software craft.</p>

<h2 id="what-ai-agents-really-change">What AI-Agents really change</h2>

<p>The adoption of AI-assisted coding relieves this economic pressure. An AI-Agent doesn’t iterate two or three times on a feature, it iterates twenty or thirty times, and can implement it in a few hours instead of days. This is technically impressive. But it is a brute-force approach to the problem. Instead of reducing the problem scope through careful design, the solution space is explored through sheer computational power.</p>

<p>The principle is not new. Brute-Force approaches are a legitimate strategy, as long as the problem space is small enough. But software design does not have a small - or even bounded - problem space. The combination of requirements, architectures, use cases and systematic constraints are exponential. And so is the needed power to cover this with a brute-force approach.</p>

<p>One could argue that reflection hasn’t disappeared, it has just moved into the prompt: writing a good request requires thinking too. But specifying a request is not the same act as designing a system.</p>

<blockquote>
  <p>The old reflection didn’t come from developers being virtuous, it came from hitting resistance while building.</p>
</blockquote>

<p>You’d start modeling a piece of data, run into an edge case mid-implementation, and that friction would force a redesign. Agentic coding absorbs that friction on the agent’s side: the human never hits the wall that used to trigger the rethink, because the wall gets hit and silently worked around inside the AI’s iteration loop. And design thinking has always depended on stepping away from the keyboard: sleeping on a decision, sketching it out, arguing about it with a colleague. The faster and tighter the prompt-to-code loop gets, the less room there is for that incubation. Prompting is optimized for continuous momentum, which is structurally opposed to the pause that produces the best design decisions.</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>

<h2 id="trading-thinking-time-for-production-time">Trading thinking time for production time</h2>

<p>That lost friction doesn’t just remove a delay, it skips the specific questions it used to force: is this really needed? Is it the right approach, and is it the right time? Which edge-cases are relevant, which trade-offs are acceptable, and which requirements actually matter if they change? Agentic coding jumps straight over these questions. Humans front-feed requirements to the AI-Agent, and the AI-Agent produces code: the code is written and in production before the questions even get asked. And why not - if it proves to be a wrong decision, the AI-Agent can just iterate again and produce a new version.</p>

<p>The problem here is that the most expensive part of developing and running software are often not bugs, but incorrect requirements, dead code that needs maintenance and accidental complexity in products.</p>

<blockquote>
  <p>The cost of a wrong decision is not the cost of fixing a bug, but the cost of maintaining a wrong decision for years.</p>
</blockquote>

<p>And this is where agentic coding fails: it does not help to make better decisions, it only helps to implement them faster. <strong>Implementing a feature, that nobody needs in lightning fast time is not efficiency it is fully automated waste of resources.</strong></p>

<h2 id="other-symptoms-to-watch-out-for">Other symptoms to watch out for</h2>

<p>We all cheer the speed of agentic coding, but we should also watch out for the symptoms of a growing problem. There are several other symptoms that indicate that we are heading for a smelly place in software engineering.</p>

<p>Teams are losing collective understanding of the system they are trying to build and systems are losing their architectural cohesion. For a long time system understanding was gained by building the system itself. The act of implementing, testing and refactoring a system was the way to gain understanding of it. If implementation is treated as an afterthought, systems become black boxes within their own product. The team is degraded to archivists of code they no longer understand.</p>

<p>With this brute force approach where AI-Agents optimize for local goals, the overall system architecture is at risk and software becomes bloated. As a result operating costs will rise and the system will become more fragile. The cost of maintaining a system is not just the cost of fixing bugs, but also the cost of understanding it.</p>

<blockquote>
  <p>If the system becomes too complex to understand, it becomes too expensive to maintain.</p>
</blockquote>

<p>Technical debts rise faster and so does the interest paid into it through refactoring. Even with assisted refactorings if they are large enough they carry risks in destabilizing a running system. Watching out for code churn and the growth of the code base is a good indicator for the health of a software project.</p>

<p>A common pitch from AI-tooling vendors is deceptively simple: just wrap the whole process into another AI-Agent that is responsible for the overall system architecture. But “just add another layer of abstraction” is a long known anti-pattern in software engineering. It is again a brute-force approach to a problem that is not bounded.</p>

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

<p>So what is the solution? Should we stop using AI-assisted coding? No, it is a productivity boost and it can help to implement features faster. But it does not eliminate the need for good design, reflection and above all it does not invalidate many of the practices around good software craft.</p>

<blockquote>
  <p>The answer is not more AI, but more discipline in software engineering.</p>
</blockquote>

<p>The practices that were adopted because implementation was expensive are still valid, even if implementation becomes cheap. The need for good design, reflection and discipline in software engineering is not going away. It is just that the economic pressure that forced us to adopt these practices has shifted. If implementation becomes cheap and we don’t stop to think, we will end up in a world of bloated, fragile and unmaintainable software.</p>

<p>The term “software craft” has two dimensions: The craft of producing running software and the intellectual judgment on what to produce in the first place. The first dimension is what AI-assisted coding is good at, the second dimension is what it is bad at. The question whether a system is still suited to its purpose in a few years from now is not a question of implementation speed and will remain a question of human judgment in many cases. If you think agentic coding is a silver bullet for software engineering, you are mistaking speed for direction and you might be in for a rude awakening.</p>]]></content><author><name>Dominik Berner</name></author><category term="agile" /><category term="software-delivery" /><summary type="html"><![CDATA[Writing code faster is not the same as delivering better software. There is an inconvenient truth about agentic and AI-assisted coding: it is brute-force software engineering. It can help you write code faster, but it does not help you deliver better software faster. Working with AI-assisted coding is a productivity boost. Features get implemented in mere hours instead of days and weeks, each iteration on the software becomes cheaper and faster. This sounds like the holy grail of software development, but is it?]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://softwarecraft.ch//images/agentic-coding-brute-force/thumbnail.jpg" /><media:content medium="image" url="https://softwarecraft.ch//images/agentic-coding-brute-force/thumbnail.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><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></feed>