<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://rehsack.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://rehsack.dev/" rel="alternate" type="text/html" /><updated>2026-04-09T19:41:13+00:00</updated><id>https://rehsack.dev/feed.xml</id><title type="html">Jens Rehsack</title><subtitle>Technical blog and project showcase by Jens Rehsack</subtitle><author><name>Jens Rehsack</name></author><entry><title type="html">Yocto for bare-metal: cross-compiler and CI toolchain from a single source</title><link href="https://rehsack.dev/2026/04/09/yocto-bare-metal-sdk-from-a-single-source/" rel="alternate" type="text/html" title="Yocto for bare-metal: cross-compiler and CI toolchain from a single source" /><published>2026-04-09T00:00:00+00:00</published><updated>2026-04-09T00:00:00+00:00</updated><id>https://rehsack.dev/2026/04/09/yocto-bare-metal-sdk-from-a-single-source</id><content type="html" xml:base="https://rehsack.dev/2026/04/09/yocto-bare-metal-sdk-from-a-single-source/"><![CDATA[<div class="paragraph">
<p>Building firmware for microcontrollers requires a cross-compiler.
You can get one from <a href="https://crosstool-ng.github.io/">crosstool-ng</a>,
as an ARM binary download, or from your distribution&#8217;s package
manager.  That works&#8201;&#8212;&#8201;and once you want unit tests in CI as well,
things get really interesting.</p>
</div>
<div class="paragraph">
<p>Because then you need <em>two</em> toolchains: one for the target
(<code>arm-none-eabi-gcc</code> with newlib) and one for the host (<code>x86_64</code>
or <code>aarch64</code>) to compile the same code against a test framework.
The host naturally runs a different libc&#8201;&#8212;&#8201;but the GCC generation,
Google Test version, gcov, and the analysis tools must match exactly.</p>
</div>
<div class="paragraph">
<p>Bare-metal sounds like minimal effort: when newlib is already
the gold standard, the horizon feels within arm&#8217;s reach.  The
challenge is not the number of dependencies, but the fact that all
the surrounding tools must be in lockstep for both target and host.
And that can be solved elegantly.</p>
</div>
<div class="sect1">
<h2 id="the-problem-two-toolchains-one-truth">The problem: two toolchains, one truth</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Say the cross-compiler is GCC 15 with newlib.  Unit tests run on the
CI host, so we need GCC 15 there as well&#8201;&#8212;&#8201;not the GCC 14 that
Fedora 43 ships, and certainly not the GCC 13 from the Ubuntu LTS
on the CI runners.</p>
</div>
<div class="paragraph">
<p>On top of that come ABI-coupled tools:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>Google Test / GMock</strong>&#8201;&#8212;&#8201;compiled against the respective toolchain</p>
</li>
<li>
<p><strong>gcov / lcov</strong>&#8201;&#8212;&#8201;must match the GCC version, otherwise coverage data is off</p>
</li>
<li>
<p><strong>valgrind</strong>&#8201;&#8212;&#8201;must understand the ABI of the tested binaries</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Maintaining all of this manually means target and host toolchain
quickly end up in separate supply chains.  It doesn&#8217;t have to be
that way.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="why-i-approach-this-differently-today">Why I approach this differently today</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Some years ago at a telecom equipment vendor: a SoM with network
accelerators, plus a binary SDK from the chip manufacturer.  32-bit,
an ancient GCC version, linked against a Linux distribution only
available on the black market in exchange for a handsome number of
camels.  Long before LLVM/Clang became a serious GCC contender and
shook up the stagnation.</p>
</div>
<div class="paragraph">
<p>IT ran a multi-processor node with about 64 VMs&#8201;&#8212;&#8201;2&#160;VCPUs, 2&#160;GB RAM each, massively overcommitted, barely
maintainable.  The SDK could not be reproduced, updated, or ported
to another host platform.  And the chip was even Linux-capable&#8201;&#8212;&#8201;much more would have been possible.</p>
</div>
<div class="paragraph">
<p>I have seen this pattern repeatedly since then: chip manufacturers
want to sell chips, not development environments.  The SDK is a
side dish&#8201;&#8212;&#8201;it eases the initial setup but is not meant to carry
you long-term.  A vendor BSP on an LTS base, good enough for
years&#8201;&#8212;&#8201;until the LTS cycle expires.  Then suddenly: we need to
update, ship in six weeks, and it must not cost anything.</p>
</div>
<div class="paragraph">
<p>That motivated me to take a different path.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="the-yocto-solution-machine-abstraction-as-ssot">The Yocto solution: MACHINE abstraction as SSOT</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The <a href="https://www.yoctoproject.org/">Yocto Project</a> is primarily known
as a build system for embedded Linux distributions.  But its recipe
system goes further: it builds <em>arbitrary</em> toolchains from source&#8201;&#8212;&#8201;for different target architectures, from the same set of recipes.</p>
</div>
<div class="paragraph">
<p>The key is the <code>MACHINE</code> variable.  The same recipes for GCC, newlib,
Google Test, and gcov produce different results depending on the
target:</p>
</div>
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 20%;">
<col style="width: 40%;">
<col style="width: 40%;">
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">MACHINE</th>
<th class="tableblock halign-left valign-top">Result</th>
<th class="tableblock halign-left valign-top">Example</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>aducm360</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>arm-none-eabi-gcc</code> (Cortex-M3, soft-float)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">measurement MCU, ADC front-end</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>stm32h7a3</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>arm-none-eabi-gcc</code> (Cortex-M7, hard-float, FPv5-D16)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">communication MCU, DCP stack</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>samd21</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>arm-none-eabi-gcc</code> (Cortex-M0+, soft-float)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">camera control</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>esp32c6</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>riscv32-none-elf-gcc</code> (RV32IMAC)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">RISC-V IoT, Wi-Fi/BLE</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>gd32vf103</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>riscv32-none-elf-gcc</code> (RV32IMAC)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">RISC-V with DSP extensions, atomthreads</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>x86-64</code> / <code>aarch64</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">host-native GCC of the same generation</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">CI: unit tests, valgrind, coverage</p></td>
</tr>
</tbody>
</table>
<div class="paragraph">
<p>The list could go on.  The point is: whether ARM Cortex-M, RISC-V,
or host-native&#8201;&#8212;&#8201;the recipes are the same.  Only <code>MACHINE</code> and the
associated tune configuration differ.</p>
</div>
<div class="paragraph">
<p><strong>One</strong> source, <strong>one</strong> GCC version, <strong>one</strong> ABI coupling&#8201;&#8212;&#8201;DRY and SSOT
instead of n&#160;separately maintained toolchains.</p>
</div>
<div class="paragraph">
<p>Yocto does not just build the compiler: a <strong>QEMU</strong> for the respective
target can be produced from the same recipes.  This lets you emulate
firmware binaries on the CI host&#8201;&#8212;&#8201;without real hardware, but with
the correct CPU emulation.  For integration tests on a Cortex-M or
RISC-V, that is pure gold.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="what-belongs-in-the-sdkand-what-doesnt">What belongs in the SDK&#8201;&#8212;&#8201;and what doesn&#8217;t?</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The first instinct is: only the compiler and its immediate
dependencies.  In practice, the boundary extends further.</p>
</div>
<div class="paragraph">
<p><strong>From Yocto&#8201;&#8212;&#8201;frozen and versioned:</strong></p>
</div>
<div class="ulist">
<ul>
<li>
<p>GCC + binutils + newlib (the compiler core)</p>
</li>
<li>
<p>Google Test / GMock (test framework, compiled against the toolchain)</p>
</li>
<li>
<p>gcov / lcov (coverage&#8201;&#8212;&#8201;must match the GCC version)</p>
</li>
<li>
<p>valgrind (memory analysis&#8201;&#8212;&#8201;must understand the ABI)</p>
</li>
<li>
<p><strong>clang-tidy</strong> (static analysis&#8201;&#8212;&#8201;needs the <em>correct</em> target
headers and libraries, not those of the host system)</p>
</li>
<li>
<p><strong>clang-format</strong> (a frozen version prevents whitespace wars
between developers)</p>
</li>
<li>
<p><strong>CMake, Ninja</strong> (if the host distribution&#8217;s CMake suddenly gets a
major update, it might break the build)</p>
</li>
<li>
<p>QEMU (target emulation for integration tests)</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>The common denominator: everything that affects the build result or
the analysis of the build result must be freezable and reproducible.
clang-tidy sounds orthogonal at first&#8201;&#8212;&#8201;until you realise it needs
to parse the target headers, not those of the host system.</p>
</div>
<div class="paragraph">
<p><strong>From the distribution&#8201;&#8212;&#8201;independently updatable:</strong></p>
</div>
<div class="ulist">
<ul>
<li>
<p>Doxygen (API documentation)</p>
</li>
<li>
<p>srecord (ELF &#8594; HEX conversion)</p>
</li>
<li>
<p>doctoolchain, asciidoctor (project documentation)</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>These tools affect neither the binary nor the analysis and can be
updated independently&#8201;&#8212;&#8201;without firmware requalification.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="bare-metal-hardening-what-works-without-an-os">Bare-metal hardening: what works without an OS?</h2>
<div class="sectionbody">
<div class="paragraph">
<p>A side benefit of the Yocto-based toolchain: full control over
compiler flags, in clean layers.</p>
</div>
<div class="paragraph">
<p>On bare-metal the usual OS protection mechanisms are absent
(<code><em>_stack_chk_fail</code> without libc, no ASLR without a loader, no PIE
without an MMU).  But GCC 13+ offers hardening flags that work
_without</em> an OS:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="bash"><span class="nt">-fharden-compares</span>
<span class="nt">-fharden-conditional-branches</span>
<span class="nt">-fharden-control-flow-redundancy</span>
<span class="nt">-ftrivial-auto-var-init</span><span class="o">=</span>zero</code></pre>
</div>
</div>
<div class="paragraph">
<p>These flags do not go into the toolchain itself (otherwise newlib
would not build), but into an SDK environment hook&#8201;&#8212;&#8201;a thin shell
file that sets <code>CFLAGS</code>/<code>CXXFLAGS</code> when the toolchain is activated.
Three layers:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p><strong>Distro config</strong> (Yocto): flags that newlib also needs (<code>-ffunction-sections</code>, <code>-fdata-sections</code>)</p>
</li>
<li>
<p><strong>SDK hook</strong>: hardening and warnings for firmware development</p>
</li>
<li>
<p><strong>Firmware project</strong> (CMakeLists.txt): project-specific settings (<code>-std=gnu23</code>, target defines)</p>
</li>
</ol>
</div>
</div>
</div>
<div class="sect1">
<h2 id="long-term-impact-10-years-of-firmware-support">Long-term impact: 10 years of firmware support</h2>
<div class="sectionbody">
<div class="paragraph">
<p>In regulated industries you deliver firmware fixes for products
shipped 10 or 15 years ago.  With a Yocto-based toolchain that is
a <code>git checkout</code> to the release tag and a <code>bitbake meta-toolchain</code>&#8201;&#8212;&#8201;done.</p>
</div>
<div class="paragraph">
<p>Yocto provides two mechanisms often dismissed as implementation
details, but essential for release management:</p>
</div>
<div class="dlist">
<dl>
<dt class="hdlist1"><code>DL_DIR</code></dt>
<dd>
<p>The directory where Yocto stores all downloaded sources&#8201;&#8212;&#8201;tarballs, git snapshots, patches.  This is not a by-product, it
is a <strong>source archive</strong>.  Archive <code>DL_DIR</code> and you can still build
the toolchain when the upstream servers have long gone offline.</p>
</dd>
<dt class="hdlist1"><code>SSTATE_DIR</code></dt>
<dd>
<p>The shared-state cache.  Yocto stores the
intermediate results of every build step here.  For a release fix
years later, not everything needs to be rebuilt from scratch&#8201;&#8212;&#8201;the
cache speeds up the rebuild to only the actually changed components.</p>
</dd>
</dl>
</div>
<div class="paragraph">
<p>Together, both directories form the backbone of long-term
serviceability: sources secured, build results traceable, rebuild
times manageable.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="cra-and-sbom-from-nice-to-have-to-mandatory">CRA and SBOM: from nice-to-have to mandatory</h2>
<div class="sectionbody">
<div class="paragraph">
<p>With the <a href="https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act">Cyber Resilience Act</a>
the question "what is inside my toolchain?" becomes regulatory.
An SBOM (Software Bill of Materials) with full provenance is no
longer optional&#8201;&#8212;&#8201;it is becoming mandatory for products with digital
elements.</p>
</div>
<div class="paragraph">
<p>Yocto generates SPDX SBOMs as a by-product of the build&#8201;&#8212;&#8201;for every
component built from source.  <code>DL_DIR</code> provides the source archival,
the recipes provide the dependency chain, and
<code>INHERIT += "create-spdx"</code> ties both into a machine-readable SBOM.</p>
</div>
<div class="paragraph">
<p>Built with Yocto means: provenance by construction, not by trust.
That carries real weight when the auditor comes knocking.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="conclusion">Conclusion</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Cross-compilers for bare-metal are a dime a dozen.  The real value
of Yocto is not the cross-compiler itself, but the fact that the
target toolchain, the host CI toolchain, and all surrounding tools
come <em>from the same source</em>&#8201;&#8212;&#8201;reproducible, freezable, CRA-ready.</p>
</div>
<div class="paragraph">
<p>The barrier to entry is real: Yocto has a steep learning curve.  But
those who take the leap get a toolchain infrastructure that scales
with every new target and every new project&#8201;&#8212;&#8201;instead of starting
from scratch each time.</p>
</div>
</div>
</div>]]></content><author><name>Jens Rehsack</name></author><category term="yocto" /><category term="embedded" /><category term="bare-metal" /><category term="ci" /><category term="toolchain" /><category term="cross-compilation" /><summary type="html"><![CDATA[Building firmware for microcontrollers requires a cross-compiler. You can get one from crosstool-ng, as an ARM binary download, or from your distribution&#8217;s package manager. That works&#8201;&#8212;&#8201;and once you want unit tests in CI as well, things get really interesting.]]></summary></entry><entry><title type="html">Jekyll + AsciiDoc in a devcontainer — zero local setup</title><link href="https://rehsack.dev/2026/04/05/devcontainer-asciidoc-jekyll/" rel="alternate" type="text/html" title="Jekyll + AsciiDoc in a devcontainer — zero local setup" /><published>2026-04-05T00:00:00+00:00</published><updated>2026-04-05T00:00:00+00:00</updated><id>https://rehsack.dev/2026/04/05/devcontainer-asciidoc-jekyll</id><content type="html" xml:base="https://rehsack.dev/2026/04/05/devcontainer-asciidoc-jekyll/"><![CDATA[<div class="paragraph">
<p>Setting up a Jekyll site with AsciiDoc support usually means installing Ruby,
Bundler, and a handful of gems on your machine.  With the
<a href="https://github.com/tpo42/tpo42-asciidoc-container">tpo42/adoc</a> container and the
<a href="https://containers.dev/">Dev Containers</a> specification, you can skip all of
that.</p>
</div>
<div class="sect1">
<h2 id="the-idea">The idea</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The <code>ghcr.io/tpo42/adoc</code> container already ships Ruby 3.x, Bundler 4.x, and the full
Asciidoctor toolchain (asciidoctor, asciidoctor-pdf, asciidoctor-diagram, …).
Jekyll and its plugins are just a <code>bundle install</code> away&#8201;&#8212;&#8201;no need to pollute
your host system.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="setup">Setup</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="1-gemfile">1. Gemfile</h3>
<div class="paragraph">
<p>A standard Jekyll Gemfile&#8201;&#8212;&#8201;the container provides Ruby and Bundler, so
nothing else is needed on the host:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="ruby"><span class="n">source</span> <span class="s2">"https://rubygems.org"</span>

<span class="n">gem</span> <span class="s2">"jekyll"</span><span class="p">,</span> <span class="s2">"~&gt; 4.4"</span>
<span class="n">gem</span> <span class="s2">"jekyll-remote-theme"</span>
<span class="n">gem</span> <span class="s2">"jekyll-asciidoc"</span>
<span class="n">gem</span> <span class="s2">"asciidoctor"</span>
<span class="n">gem</span> <span class="s2">"jekyll-sitemap"</span>
<span class="n">gem</span> <span class="s2">"jekyll-feed"</span>
<span class="n">gem</span> <span class="s2">"jekyll-include-cache"</span>
<span class="n">gem</span> <span class="s2">"webrick"</span></code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="2-devcontainerdevcontainer-json">2. <code>.devcontainer/devcontainer.json</code></h3>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="json"><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"my-site"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"image"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ghcr.io/tpo42/adoc:latest"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"workspaceFolder"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/workspace"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"workspaceMount"</span><span class="p">:</span><span class="w"> </span><span class="s2">"source=${localWorkspaceFolder},target=/workspace,type=bind"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"postCreateCommand"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bundle config set --local path vendor/bundle &amp;&amp; bundle install"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"appPort"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">4000</span><span class="p">,</span><span class="w"> </span><span class="mi">35729</span><span class="p">],</span><span class="w">
  </span><span class="nl">"forwardPorts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">4000</span><span class="p">,</span><span class="w"> </span><span class="mi">35729</span><span class="p">],</span><span class="w">
  </span><span class="nl">"portsAttributes"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"4000"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"label"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Jekyll"</span><span class="p">,</span><span class="w"> </span><span class="nl">"onAutoForward"</span><span class="p">:</span><span class="w"> </span><span class="s2">"notify"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"35729"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"label"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LiveReload"</span><span class="p">,</span><span class="w"> </span><span class="nl">"onAutoForward"</span><span class="p">:</span><span class="w"> </span><span class="s2">"silent"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span></code></pre>
</div>
</div>
<div class="paragraph">
<p>Key points:</p>
</div>
<div class="dlist">
<dl>
<dt class="hdlist1"><code>image</code></dt>
<dd>
<p>Uses <code>ghcr.io/tpo42/adoc:latest</code> directly (the "simple case" from
ADR-005).  No custom Dockerfile needed.</p>
</dd>
<dt class="hdlist1"><code>postCreateCommand</code></dt>
<dd>
<p>Installs gems into <code>vendor/bundle</code> (writable by
the container user, ignored by <code>.gitignore</code>).</p>
</dd>
<dt class="hdlist1"><code>appPort</code></dt>
<dd>
<p>Maps ports at the Docker level, so <code>jekyll serve</code> is
reachable from the host even without VS Code.</p>
</dd>
<dt class="hdlist1"><code>forwardPorts</code></dt>
<dd>
<p>Does the same for VS Code&#8217;s built-in port forwarding.</p>
</dd>
</dl>
</div>
</div>
<div class="sect2">
<h3 id="3-start-the-container">3. Start the container</h3>
<div class="paragraph">
<p>With the <a href="https://github.com/devcontainers/cli">devcontainer CLI</a>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="bash">devcontainer up <span class="nt">--workspace-folder</span> .</code></pre>
</div>
</div>
<div class="paragraph">
<p>Then serve the site:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="bash">devcontainer <span class="nb">exec</span> <span class="nt">--workspace-folder</span> <span class="nb">.</span> <span class="se">\</span>
  bundle <span class="nb">exec </span>jekyll serve <span class="nt">--host</span> 0.0.0.0 <span class="nt">--livereload</span></code></pre>
</div>
</div>
<div class="paragraph">
<p>Open <a href="http://localhost:4000&#8201;&#8212;&#8201;done" class="bare">http://localhost:4000&#8201;&#8212;&#8201;done</a>.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="why-not-a-dedicated-jekyll-image">Why not a dedicated Jekyll image?</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Images like <code>bretfisher/jekyll-serve</code> work, but they ship their own Ruby and
gem set.  If your content is AsciiDoc, you end up needing the Asciidoctor
toolchain <em>and</em> Jekyll&#8201;&#8212;&#8201;two separate containers, or one bloated custom image.</p>
</div>
<div class="paragraph">
<p>With <code>ghcr.io/tpo42/adoc</code> as the base, you get Asciidoctor for document processing
(flatten, validate, extract diagrams via <code>adcw</code>) <em>and</em> Jekyll for serving&#8201;&#8212;&#8201;from a single image, driven by your project&#8217;s Gemfile.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="asciidoc-validationstill-works-from-the-host">AsciiDoc validation&#8201;&#8212;&#8201;still works from the host</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The <code>adcw</code> wrapper does not require a devcontainer.  You can validate your
<code>.adoc</code> files any time:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="bash">adcw validate <span class="nt">-i</span> _pages/about.adoc</code></pre>
</div>
</div>
<div class="paragraph">
<p>This runs a quick <code>docker run --rm</code>&#8201;&#8212;&#8201;no long-lived container needed.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="lessons-learned">Lessons learned</h2>
<div class="sectionbody">
<div class="dlist">
<dl>
<dt class="hdlist1"><code>appPort</code> vs <code>forwardPorts</code></dt>
<dd>
<p>The devcontainer CLI does not forward ports on its own.  <code>forwardPorts</code> is a
VS Code feature.  For CLI-only workflows, <code>appPort</code> is required.</p>
</dd>
<dt class="hdlist1">Gem permissions</dt>
<dd>
<p>The <code>ghcr.io/tpo42/adoc</code> container runs as an unprivileged user.  <code>bundle install</code>
without <code>--path</code> tries to write to system gem directories and fails.
<code>bundle config set --local path vendor/bundle</code> solves this cleanly.</p>
</dd>
<dt class="hdlist1">Container reuse</dt>
<dd>
<p><code>devcontainer up</code> reuses existing containers.  After changing
<code>devcontainer.json</code>, remove the old container first (<code>docker rm -f &lt;id&gt;</code>)
before running <code>devcontainer up</code> again.</p>
</dd>
</dl>
</div>
</div>
</div>]]></content><author><name>Jens Rehsack</name></author><category term="devcontainer" /><category term="jekyll" /><category term="asciidoc" /><category term="docker" /><category term="tpo42" /><summary type="html"><![CDATA[Setting up a Jekyll site with AsciiDoc support usually means installing Ruby, Bundler, and a handful of gems on your machine. With the tpo42/adoc container and the Dev Containers specification, you can skip all of that.]]></summary></entry></feed>