<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bruno Verachten</title>
    <description>The latest articles on DEV Community by Bruno Verachten (@gounthar).</description>
    <link>https://dev.to/gounthar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F119512%2F56f3a9de-2488-4302-ae9a-a97b3b037299.jpeg</url>
      <title>DEV Community: Bruno Verachten</title>
      <link>https://dev.to/gounthar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gounthar"/>
    <language>en</language>
    <item>
      <title>Above this line: a lift ride under your hypervisor</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Thu, 05 Nov 2026 11:14:32 +0000</pubDate>
      <link>https://dev.to/gounthar/above-this-line-a-lift-ride-under-your-hypervisor-3mfl</link>
      <guid>https://dev.to/gounthar/above-this-line-a-lift-ride-under-your-hypervisor-3mfl</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on November 5, 2026 at &lt;a href="https://bruno.verachten.fr/2026/11/05/above-this-line-a-lift-ride-under-your-hypervisor/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2026/11/05/above-this-line-a-lift-ride-under-your-hypervisor/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;▶ 0. The lift ride&lt;/strong&gt; (you are here)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;-1. One setting changed, two lines moved (26 November)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;-2. The roommate in the closet (12 November)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monday morning. No &lt;code&gt;:latest&lt;/code&gt; for firmware (19 November)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RISC-V. Down to the last binary (3 December)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F11%2F05%2F00-header-mine-shaft.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F11%2F05%2F00-header-mine-shaft.jpg" alt="Looking straight down a square timber mine shaft, wooden frames receding floor after floor towards dark water at the bottom, a blue cable hanging down the middle" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo: Orphan Girl Mine, Butte, Montana, by &lt;a href="https://commons.wikimedia.org/wiki/File:Flooded_mine_shaft_(Orphan_Girl_Mine,_Butte,_Montana,_USA)_3.jpg" rel="noopener noreferrer"&gt;James St. John&lt;/a&gt;, &lt;a href="https://creativecommons.org/licenses/by/2.0/" rel="noopener noreferrer"&gt;CC BY 2.0&lt;/a&gt;, resized.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In October 2025, Ruter, the public transport operator in Oslo, asked itself a very simple question: who actually controls our electric buses? So they took two of their own, a brand-new Yutong built in China and a three-year-old VDL from the Netherlands, and drove them into an isolated environment inside a mountain. No network. Just two buses, some probes, and a lot of rock.&lt;/p&gt;

&lt;p&gt;What did they find? The new one was built for over-the-air updates: its manufacturer could reach each bus directly, battery and power-supply control system included, over the mobile network, through a Romanian SIM card. (A Chinese bus, in Oslo, talking through Romania. I’ve stopped trying to make the geography work.) The old one had no OTA at all.&lt;/p&gt;

&lt;p&gt;Ruter’s conclusion, in &lt;a href="https://web.archive.org/web/2025/https://ruter.no/nyheter/ruter-med-omfattende-sikkerhetstest-av-elektriske-busser" rel="noopener noreferrer"&gt;its own publication&lt;/a&gt;, is that the manufacturer could, in theory, stop the bus or render it unusable. In theory. Nobody switched off a bus. But keep the shape of it in mind: closed firmware that somebody else can update.&lt;/p&gt;

&lt;p&gt;Hold on to the bus. It comes back.&lt;/p&gt;

&lt;p&gt;Ruter wondered who controls its buses. I asked the same thing about servers, mine and yours, to open my &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=6s" rel="noopener noreferrer"&gt;talk at Cloud Nord 2026&lt;/a&gt; in Lille. This is the written version (the &lt;a href="https://speakerdeck.com/gounthar/100-percent-open-source-au-dessus-de-cette-ligne" rel="noopener noreferrer"&gt;slides&lt;/a&gt; are online, in French, sources in the appendix).&lt;/p&gt;

&lt;h1&gt;
  
  
  Hands up, hands down
&lt;/h1&gt;

&lt;p&gt;First question to the room: who has ever opened a BMC’s web interface? (The BMC is the little board that lets you power a server on remotely, and a lot more.) A handful of hands went up. Then: who has ever read the code running inside it?&lt;/p&gt;

&lt;p&gt;Every hand dropped.&lt;/p&gt;

&lt;p&gt;That’s the whole talk, in two questions. (If you only run cloud VMs and have never touched a BMC: lucky you, there’s a paragraph for you further down.)&lt;/p&gt;

&lt;p&gt;I’m Bruno, community manager at Vates (we make XCP-ng and Xen Orchestra). At Cloud Nord 2025 I was turning Android phones into Jenkins agents; this year I came back to teach people to distrust their real, perfectly legit servers. « Je progresse dans la paranoïa, c’est l’âge. » &lt;em&gt;I’m getting more paranoid. It’s my age.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I’m no firmware expert, by the way: nearly everything below, I discovered from summer 2026 onwards, so this series is me doing my homework in public.&lt;/p&gt;

&lt;h1&gt;
  
  
  The method: the "descenseur"
&lt;/h1&gt;

&lt;p&gt;We take the lift from the VM all the way down to the silicon. On stage I called it « le descenseur » (French says &lt;em&gt;ascenseur&lt;/em&gt;, and we were only ever going down). Think strace, except we don’t stop at the kernel. On each floor, we call an imaginary function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// readable: you read it, you patch it      -&amp;gt; green&lt;/span&gt;
&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;  &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// partly: an open door exists, few use it  -&amp;gt; orange&lt;/span&gt;
&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// EACCES: you can't read it or remove it   -&amp;gt; red&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EACCES is what Linux hands you when it refuses you a file. At Cloud Nord the room voted with raised hands before each &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=340s" rel="noopener noreferrer"&gt;verdict&lt;/a&gt;. You can vote in your head. I won’t count (promise).&lt;/p&gt;

&lt;p&gt;Somewhere on the way down there’s a line. Above it, you read the code. Below it, you take the vendor’s word for it. That’s the line in the title, and on every floor we’ll check which side of it we’re standing on (spoiler: it doesn’t improve).&lt;/p&gt;

&lt;p&gt;One more map, because other people’s writing uses it. x86 counts rings from 3 (apps) down to 0 (kernel), and the hypervisor gets "ring -1", which is a figure of speech, not a piece of hardware. Libreboot calls the ME and the PSP (floor -3, patience) "ring -2" or "ring -3". Not a ring at all: another processor, reaching the centre. Rings aren’t floors, but they point the same way. Down. Audit your hypervisor all you like, you still haven’t audited the bottom.&lt;/p&gt;

&lt;h1&gt;
  
  
  Floor 0: everything’s fine
&lt;/h1&gt;

&lt;p&gt;Ground floor: your VMs, and the hypervisor underneath. Xen, KVM: &lt;a href="https://xenbits.xen.org/gitweb/?p=xen.git;a=blob;f=COPYING" rel="noopener noreferrer"&gt;GPL&lt;/a&gt;, public code. You read it, you patch it, you send the patch upstream. &lt;code&gt;audit("hypervisor") == 0&lt;/code&gt;. &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=406s" rel="noopener noreferrer"&gt;Green&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Everything’s fine…​ until you power the machine on.&lt;/p&gt;

&lt;p&gt;Even at home. On stage I’d promised to put my own product in orange, so here goes: the hypervisor’s code is green, but booting it is another story. Secure Boot is the bouncer at the firmware’s door ("Sneakers? You’re not getting in."). Turn it on, and an XCP-ng host won’t boot, installer or installed. I hit it in my lab, on an OptiPlex. First I blamed the USB stick. Then the ISO. Then the IP KVM. Best part? The ISO really was corrupt as well. Two problems, one symptom. With a clean image and Secure Boot on:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;All bootable devices failed Secure Boot verification.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Not a bug.&lt;/p&gt;

&lt;p&gt;So what is it, then? Partly signatures: XCP-ng’s boot chain isn’t signed with a key your firmware trusts out of the box. The &lt;a href="https://docs.xcp-ng.org/releases/release-8-1/" rel="noopener noreferrer"&gt;8.1 release notes&lt;/a&gt; say Secure Boot relies on proprietary packages, the &lt;a href="https://xcp-ng.org/forum/topic/8485" rel="noopener noreferrer"&gt;forum&lt;/a&gt; says it would need a valid Microsoft certificate, and the &lt;a href="https://github.com/xcp-ng/xcp/issues/294" rel="noopener noreferrer"&gt;request&lt;/a&gt; has been open since 2019. Guests have had &lt;a href="https://docs.xcp-ng.org/guides/guest-UEFI-Secure-Boot/" rel="noopener noreferrer"&gt;Secure Boot&lt;/a&gt; since 8.2.1 (same name, different thing, just to keep you on your toes). And enrolling your own keys? Then you sign the whole chain, every update included.&lt;/p&gt;

&lt;p&gt;« Le Secure Boot ne vérifie pas que le code est ouvert. Il vérifie qui l’a signé. » &lt;em&gt;Secure Boot doesn’t check that the code is open. It checks who signed it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Who holds the keys. Sounds a bit like the bus, doesn’t it?&lt;/p&gt;

&lt;h1&gt;
  
  
  Floor -1: open upstream, closed at your place
&lt;/h1&gt;

&lt;p&gt;Knock knock. On &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=506s" rel="noopener noreferrer"&gt;floor -1&lt;/a&gt;, the firmware that boots the machine answers. And your vendor’s UEFI? A closed binary. On 22 October 2026, TianoCore published &lt;a href="https://github.com/tianocore/edk2/security/advisories/GHSA-99cj-7hg9-mp4m" rel="noopener noreferrer"&gt;GHSA-99cj-7hg9-mp4m&lt;/a&gt;, a critical pre-boot flaw in the PXE driver of EDK2, the reference UEFI implementation. The &lt;a href="https://github.com/tianocore/edk2/commit/560ed9b5b1" rel="noopener noreferrer"&gt;fix&lt;/a&gt; had been on master since 15 October. In your server, though, it lands when your vendor ships a new binary. Not before. (Dan Goodin at Ars Technica &lt;a href="https://arstechnica.com/security/2026/10/code-execution-flaw-in-uefi-firmware-threatens-most-servers-on-planet-earth/" rel="noopener noreferrer"&gt;explains why&lt;/a&gt; that reaches so many servers: most UEFI suppliers build on EDK2.)&lt;/p&gt;

&lt;p&gt;« Ouvert en amont, fermé chez vous. » &lt;em&gt;Open upstream, closed at your place.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Why orange, then, and not red? Because doors exist. &lt;a href="https://doc.coreboot.org/" rel="noopener noreferrer"&gt;coreboot&lt;/a&gt; does the bare minimum of hardware bring-up, then hands over to a payload such as TianoCore, which &lt;em&gt;is&lt;/em&gt; EDK2, compiled by you this time. &lt;a href="https://libreboot.org/" rel="noopener noreferrer"&gt;Libreboot&lt;/a&gt; is to coreboot what Debian is to Linux: a distribution of it, the purist one, mostly on older machines (hello, ThinkPad X200 from 2008). On recent Intel, memory init goes through Intel’s FSP blob, and the &lt;a href="https://libreboot.org/faq.html" rel="noopener noreferrer"&gt;Libreboot FAQ&lt;/a&gt; says replacing it would take the world’s best developer years. (Not me, then.) &lt;a href="https://dasharo.com/" rel="noopener noreferrer"&gt;Dasharo&lt;/a&gt;, also coreboot-based, ships on laptops and on a handful of server boards with paid support.&lt;/p&gt;

&lt;p&gt;Dasharo can even stop the Intel ME after boot, with the &lt;a href="https://docs.dasharo.com/osf-trivia-list/me/" rel="noopener noreferrer"&gt;HAP bit&lt;/a&gt;, a flag that tells the ME to halt once the hardware is up. Stopped. Not removed. We’ll meet it again two floors down.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F11%2F05%2Fdasharo-intel-me-mode-menu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F11%2F05%2Fdasharo-intel-me-mode-menu.jpeg" alt="A blue and grey firmware setup screen titled Intel Management Engine Options, with the Intel ME mode drop-down open on three choices: Enabled, Disabled (Soft) and Disabled (HAP)" width="643" height="459"&gt;&lt;/a&gt;&lt;/p&gt;
Dasharo’s setup menu offers three Intel ME modes. Screenshot: 3mdeb, from the &lt;a href="https://github.com/Dasharo/docs/blob/31300a0/docs/images/menus/me_menu2.jpeg" rel="noopener noreferrer"&gt;Dasharo documentation&lt;/a&gt;, &lt;a href="https://www.apache.org/licenses/LICENSE-2.0" rel="noopener noreferrer"&gt;Apache-2.0&lt;/a&gt;.



&lt;h1&gt;
  
  
  Floor -2: the roommate in the closet
&lt;/h1&gt;

&lt;p&gt;Who has ever updated their BMC’s firmware? Who knows which OS runs inside it? (Before this talk, not me. Not a clue.)&lt;/p&gt;

&lt;p&gt;Remember the Romanian SIM? Your server has one too: the &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=711s" rel="noopener noreferrer"&gt;BMC&lt;/a&gt;. Power, console, ISOs, its own OS and its own network, even while the server is off. A roommate living in your closet, with the house keys. The keys come with the server, though not always all of them: on one machine I was lent, mounting an ISO needed a paid licence. That same machine, with 1.5 TB of RAM, spent more than four minutes in POST (the power-on self-test, the machine checking itself) before even looking for its devices. Four minutes of code you can’t read, and your OS isn’t even born yet. Long enough to wonder what it’s doing in there.&lt;/p&gt;

&lt;p&gt;Orange anyway, thanks to &lt;a href="https://github.com/openbmc/openbmc" rel="noopener noreferrer"&gt;OpenBMC&lt;/a&gt;, an open source Linux distribution for BMCs. Facebook started it in 2014, and &lt;a href="https://engineering.fb.com/2015/03/10/open-source/introducing-openbmc-an-open-software-framework-for-next-generation-system-management/" rel="noopener noreferrer"&gt;Facebook’s own 2015 engineering post&lt;/a&gt; says Facebook runs it in production. My servers? None of them. So I booted the public image for Bletchley, a Meta board, in QEMU on my laptop. That one gets its own post.&lt;/p&gt;

&lt;h1&gt;
  
  
  Floor -3: knock knock, who’s there?
&lt;/h1&gt;

&lt;p&gt;Guessing game. A reasonable one-socket server: how many processors inside? The invoice says one. The invoice is an optimist. There’s the CPU you paid for, the BMC, your SSD’s controller, and the one that answers when you &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=978s" rel="noopener noreferrer"&gt;knock on floor -3&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Yes, a fourth one. And it was there before you.&lt;/p&gt;

&lt;p&gt;On Intel, it’s the Management Engine: a separate little computer in the chipset since 2006. It has handled part of hardware init since Nehalem, its firmware is signed by Intel, and on recent chips it runs a &lt;a href="https://positechglobal.com/en/research/analytics/disabling-intel-me-11-via-undocumented-mode/" rel="noopener noreferrer"&gt;486-derived core running MINIX&lt;/a&gt; (my favourite piece of trivia in the whole talk). Without valid signed firmware, the machine shuts down after 30 minutes, according to &lt;a href="https://github.com/corna/me_cleaner/wiki/How-does-it-work%3F" rel="noopener noreferrer"&gt;people who took it apart&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Half an hour of freedom.&lt;/p&gt;

&lt;p&gt;And AMD? No favourites here. The &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=1180s" rel="noopener noreferrer"&gt;PSP&lt;/a&gt; is an Arm core on the same die as your x86 cores, and per the &lt;a href="https://doc.coreboot.org/soc/amd/psp_integration.html" rel="noopener noreferrer"&gt;coreboot&lt;/a&gt; and &lt;a href="https://libreboot.org/faq.html" rel="noopener noreferrer"&gt;Libreboot&lt;/a&gt; docs its firmware must be AMD-signed, or your x86 cores never start.&lt;/p&gt;

&lt;p&gt;Not thirty minutes: zero.&lt;/p&gt;

&lt;p&gt;Your processor has two brains, and you only hold the keys to one.&lt;/p&gt;

&lt;p&gt;This isn’t Intel bashing or AMD bashing (I just did both, to be fair), it’s architecture. And these locks exist to protect you: anti-rollback, for one, stops anyone dragging you back to an older, vulnerable firmware. They just don’t hand you the key. (AMD friends reading this: « cœur sur vous », &lt;em&gt;much love&lt;/em&gt;. I meant it on stage too.)&lt;/p&gt;

&lt;p&gt;So I looked at a real one: an AMD EPYC Genoa belonging to a partner (not the loaner from floor -2), booted from a live key. The kernel reported its PSP fused, debug locked and anti-rollback on, all of it protective, and Secure Boot had no keys enrolled at all.&lt;/p&gt;

&lt;p&gt;No TPM fitted on that box, so fwupd’s &lt;a href="https://fwupd.github.io/libfwupdplugin/hsi.html" rel="noopener noreferrer"&gt;HSI scale&lt;/a&gt; (built with PCs in mind) stopped it at HSI:0, on step one. Ouch. Count BIOS, BMC, network cards, disk controller and SSDs, and that’s at least ten closed firmwares. « Je ne jette la pierre à personne. » &lt;em&gt;I’m not throwing stones at anyone.&lt;/em&gt; Yours probably looks much the same.&lt;/p&gt;

&lt;p&gt;To be fair, I ran the same checks on my own Intel NUC: ME version 18, manufacturing mode locked, and Boot Guard fused to the board vendor’s key, so only that vendor’s firmware boots (on &lt;em&gt;my&lt;/em&gt; NUC, yes, that one stung). It has a TPM (the small chip that keeps track of what booted), and fwupd gives it HSI:3, a better score earned partly &lt;em&gt;because&lt;/em&gt; of those vendor locks.&lt;/p&gt;

&lt;p&gt;« Plus sûr, oui. Plus ouvert, non. » &lt;em&gt;Safer, yes. More open, no.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;audit("ME/PSP") == -1&lt;/code&gt;. Red. Can’t read it, can’t remove it.&lt;/p&gt;

&lt;p&gt;Two minutes, and it tells you a lot: &lt;code&gt;fwupdmgr security&lt;/code&gt;. The HSI scale was designed for laptops and desktops, so read a server’s score with that in mind.&lt;/p&gt;

&lt;p&gt;"But Bruno, there’s me_cleaner!" (Nobody said it. I felt it.) &lt;a href="https://doc.coreboot.org/northbridge/intel/sandybridge/me_cleaner.html" rel="noopener noreferrer"&gt;me_cleaner&lt;/a&gt; strips much of the ME firmware, and the HAP bit stops the ME once the hardware is up, but by then it has already done its job. Never run it on a machine you need. For the PSP, says Libreboot, there’s no equivalent.&lt;/p&gt;

&lt;p&gt;In the cloud, this floor is still under your cluster (told you there was a paragraph for you). On AMD, a confidential VM’s SEV-SNP &lt;a href="https://docs.kernel.org/virt/coco/sev-guest.html" rel="noopener noreferrer"&gt;attestation report&lt;/a&gt; comes from firmware running in, you guessed it, the PSP. You trust the host less and the chip maker more.&lt;/p&gt;

&lt;p&gt;And here’s the part I genuinely like: with openSIL, AMD publishes its silicon initialisation code as open source, and &lt;a href="https://review.coreboot.org/c/coreboot/+/88712" rel="noopener noreferrer"&gt;coreboot boots recent EPYC chips with it&lt;/a&gt; on a few boards. A real step, and AMD is the one taking it. The PSP itself still runs AMD-signed firmware, so openSIL opens the initialisation, not the PSP. But a door that wasn’t there is now ajar: not orange yet, but the first crack in the red.&lt;/p&gt;

&lt;h1&gt;
  
  
  Floor -4: the bottom of the cave
&lt;/h1&gt;

&lt;p&gt;Knock knock…​ nothing. On x86 the instruction set is &lt;a href="https://www.sec.gov/Archives/edgar/data/2488/000119312509236705/dex102.htm" rel="noopener noreferrer"&gt;proprietary&lt;/a&gt; and core designs are secret. Nobody expected to read a Xeon’s RTL (the hardware description the chip is built from) this morning. (Good. I didn’t bring one.) &lt;code&gt;audit("ISA") == -1&lt;/code&gt;. Red.&lt;/p&gt;

&lt;p&gt;Not a law of physics, though: OpenPOWER is one open ISA, and on Raptor’s machines the &lt;a href="https://git.raptorcs.com/git/" rel="noopener noreferrer"&gt;platform firmware sources&lt;/a&gt; are published and auditable. There’s another open ISA, and I’m keeping it warm for later.&lt;/p&gt;

&lt;h1&gt;
  
  
  Express ride back up
&lt;/h1&gt;

&lt;p&gt;Here’s the map you voted on: green at the top, orange in the middle, red at the bottom. So is open source pointless? (Deliberately provocative.)&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;« L’open source, c’est une direction. Pas une case à cocher. » &lt;em&gt;Open source is a direction. Not a checkbox.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I skipped a few floors on the way down (my talk, my rules). Your SSD has its own processor and closed firmware, and in 2019 Radboud University researchers &lt;a href="https://www.ieee-security.org/TC/SP2019/papers/310.pdf" rel="noopener noreferrer"&gt;showed at IEEE S&amp;amp;P&lt;/a&gt; that several self-encrypting SSDs gave up their data without the user’s password.&lt;/p&gt;

&lt;p&gt;In October 2026, an intermediate certificate of the certificate authority eMudhra turned out to have a private key equal to 4. Yes, four. Mozilla’s &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2083449" rel="noopener noreferrer"&gt;bug 2083449&lt;/a&gt; tracks eight weak intermediates; eMudhra &lt;a href="https://emudhra.com/en/post/certificate-security-incident" rel="noopener noreferrer"&gt;attributes it&lt;/a&gt; to a script error. It was spotted because the public half was public (I checked it myself, I couldn’t resist). A 4 inside SSD firmware? Nobody would ever see it.&lt;/p&gt;

&lt;p&gt;The line moves both ways. After 2015 FCC rules on 5 GHz radios, TP-Link locked out third-party firmware entirely. The &lt;a href="https://docs.fcc.gov/public/attachments/DA-16-850A1.pdf" rel="noopener noreferrer"&gt;2016 consent decree&lt;/a&gt; brought a \$200,000 penalty (for its own firmware exceeding power limits, not for open source) and a commitment to work with the open source community and chip makers on reopening the door. On an Archer A7 sold in the US, &lt;a href="https://openwrt.org/toh/tp-link/archer_a7_v5" rel="noopener noreferrer"&gt;OpenWrt installs&lt;/a&gt; from TP-Link’s own update page. Are the two connected? I can’t prove it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Monday morning: make the red visible
&lt;/h1&gt;

&lt;p&gt;I won’t ask you to flash coreboot onto production. Your CISO would find me, and rightly so.&lt;/p&gt;

&lt;p&gt;« On ne rend pas le rouge vert, on le rend visible. » &lt;em&gt;We don’t turn red into green. We make it visible.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Five &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=2002s" rel="noopener noreferrer"&gt;verbs&lt;/a&gt;. &lt;strong&gt;Inventory&lt;/strong&gt;: list every firmware you have, and "no TPM" is a line in that list too. &lt;strong&gt;Measure&lt;/strong&gt;: Secure Boot, our bouncer from floor 0, checks who signed each step; measured boot is the CCTV camera, recording each step in the TPM. &lt;strong&gt;Attest&lt;/strong&gt;: have something else compare those recordings with what you expect, because a camera nobody watches is decoration. &lt;strong&gt;Pin&lt;/strong&gt;: friends don’t let friends use latest, firmware included. &lt;strong&gt;Isolate&lt;/strong&gt;: the BMC gets its own network, and nobody else’s. Red stays red, but you go from "I trust it" to "I know when it changes". That’s not nothing. Want to watch one setting move two lines in the TPM log? That’s the floor -1 post. The rest of Monday morning gets its own.&lt;/p&gt;

&lt;h1&gt;
  
  
  The line moves
&lt;/h1&gt;

&lt;p&gt;What if, somewhere, floor -3 opened when you knocked?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=2526s" rel="noopener noreferrer"&gt;Same lift, different building&lt;/a&gt;: RISC-V. On floor -3, where x86 keeps its ME, RISC-V puts &lt;a href="https://github.com/riscv-software-src/opensbi" rel="noopener noreferrer"&gt;OpenSBI&lt;/a&gt;, the open source reference firmware running at the machine’s most privileged level: &lt;code&gt;audit("OpenSBI") == 0&lt;/code&gt;. On floor -4, the ISA is an &lt;a href="https://docs.riscv.org/reference/isa/v20260120/unpriv/unpriv-index.html" rel="noopener noreferrer"&gt;open specification&lt;/a&gt;. The core? It depends. SiFive sells the P550 as licensed IP; CVA6 publishes its RTL, though trying it on an FPGA needs a closed vendor tool. An open ISA isn’t an open core (a confusion I used to make myself, so no judgement here).&lt;/p&gt;

&lt;p&gt;On my first Banana Pi BPI-F3, the bootloader is GPL source, at the exact commit my distribution says it built. OpenSBI is open source too, though built from a contributor’s tree. Lovely. But a &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI&amp;amp;t=2631s" rel="noopener noreferrer"&gt;second, 32-bit core&lt;/a&gt; runs &lt;code&gt;esos.elf&lt;/code&gt;, a prebuilt binary my distribution copies in, even though SpacemiT publishes its source. My most open board, and still a binary nobody here rebuilt. It gets its own post.&lt;/p&gt;

&lt;p&gt;The SpacemiT K3 announces everything a hypervisor needs: H for the CPU, AIA for interrupts, an IOMMU for devices. 16 cores!&lt;/p&gt;

&lt;p&gt;Well…​ 8.&lt;/p&gt;

&lt;p&gt;Only eight of the sixteen have the H extension. &lt;a href="https://missingno.tech" rel="noopener noreferrer"&gt;Julien Aubé&lt;/a&gt; ran the commands on his board for me, and on that one board AIA is there while Linux sees no IOMMU.&lt;/p&gt;

&lt;p&gt;Still, RISC-V servers are starting to look like servers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F11%2F05%2Frvi-summit-rack-server.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F11%2F05%2Frvi-summit-rack-server.jpg" alt="An open rack server on a trade-show stand, lid removed, rows of heatsinks and drive caddies visible, a product sheet resting on top." width="480" height="640"&gt;&lt;/a&gt;&lt;/p&gt;
A rack server with its lid off, on a stand at the RVI Summit China 2026. Photo: &lt;a href="https://www.linkedin.com/posts/ludovic-dev_riscv-opensource-rise-ugcPost-7519624662964740096-nWrE/" rel="noopener noreferrer"&gt;Ludovic Henry&lt;/a&gt;, used with his permission.



&lt;p&gt;No board? Scaleway rents bare-metal RISC-V, the &lt;a href="https://www.scaleway.com/en/elastic-metal-rv1/" rel="noopener noreferrer"&gt;Elastic Metal EM-RV1&lt;/a&gt;, launched in February 2024. According to &lt;a href="https://www.scaleway.com/en/blog/risc-v-servers-in-the-cloud/" rel="noopener noreferrer"&gt;Scaleway&lt;/a&gt;, it boots U-Boot, OpenSBI, then Linux, on top of "minimal proprietary code". &lt;a href="https://github.com/riseproject-dev/riscv-runner/blob/main/website/docs/getting-started/labels.md" rel="noopener noreferrer"&gt;RISE’s GitHub runners&lt;/a&gt; run on these machines by default, and most of my riscv64 porting goes through them.&lt;/p&gt;

&lt;h1&gt;
  
  
  The bus comes back
&lt;/h1&gt;

&lt;p&gt;Told you. After Oslo, Denmark’s Movia had &lt;a href="https://www.moviatrafik.dk/media/3topzxnw/risk-assessment-of-cybersecurity-for-ev-bus-and-ev-vehicles____final-2296728_-1.pdf" rel="noopener noreferrer"&gt;EY audit&lt;/a&gt; electric buses from several manufacturers, and found no basis to consider Chinese buses less cybersecure than the others. And the manufacturer that could least demonstrate its cybersecurity management? The Dutch one, VDL: the maker of Oslo’s bus without OTA.&lt;/p&gt;

&lt;p&gt;EY gives both halves: without OTA, nobody takes over the fleet remotely, and nobody patches, monitors or audits it either. Not having OTA doesn’t make you auditable.&lt;/p&gt;

&lt;p&gt;« Injoignable, ce n’est pas vérifiable. » &lt;em&gt;Unreachable isn’t verifiable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The risk was never the bus’s passport. It’s closed firmware that someone else writes and signs, at the heart of a critical system. And you have several in every server.&lt;/p&gt;

&lt;h1&gt;
  
  
  Takeaways
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Know which floor you’re on.&lt;/em&gt; For each layer, ask whether you can read it, patch it, or only believe it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Spend two minutes on &lt;code&gt;fwupdmgr security&lt;/code&gt;.&lt;/em&gt; It opens nothing, but it tells you where you stand.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Treat firmware like container images.&lt;/em&gt; Inventory it, pin it, never &lt;code&gt;:latest&lt;/code&gt; it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Put the BMC in its own cave.&lt;/em&gt; Separate management network, restricted access, default accounts changed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Watch the bottom of the stack.&lt;/em&gt; openSIL and RISC-V show the line can move down.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to the Cloud Nord crew for having me back, to Julien Aubé for running commands on his K3 for my slides, and to Ludovic Henry for the photo. The &lt;a href="https://www.youtube.com/watch?v=AqQ8B2DxZMI" rel="noopener noreferrer"&gt;video&lt;/a&gt; (in French) and the &lt;a href="https://speakerdeck.com/gounthar/100-percent-open-source-au-dessus-de-cette-ligne" rel="noopener noreferrer"&gt;slides&lt;/a&gt; are online. Next in the series: floor -2, where I boot a BMC on my laptop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"this post"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// sources in the slides' appendix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>firmware</category>
      <category>security</category>
      <category>bmc</category>
      <category>uefi</category>
    </item>
    <item>
      <title>A RISC-V hypervisor in slow motion: XCP-ng and Xen Orchestra, from inside QEMU</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Fri, 23 Oct 2026 20:16:57 +0000</pubDate>
      <link>https://dev.to/gounthar/a-risc-v-hypervisor-in-slow-motion-xcp-ng-and-xen-orchestra-from-inside-qemu-48bi</link>
      <guid>https://dev.to/gounthar/a-risc-v-hypervisor-in-slow-motion-xcp-ng-and-xen-orchestra-from-inside-qemu-48bi</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on October 23, 2026 at &lt;a href="https://bruno.verachten.fr/2026/10/23/a-risc-v-hypervisor-in-slow-motion-xcp-ng-and-xen-orchestra-from-inside-qemu/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2026/10/23/a-risc-v-hypervisor-in-slow-motion-xcp-ng-and-xen-orchestra-from-inside-qemu/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F00-header-long-exposure-river.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F00-header-long-exposure-river.jpg" alt="A river rushing over boulders between green wooded hills, the water blurred to a white haze by a long exposure" width="799" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by John Thomas on &lt;a href="https://unsplash.com/photos/river-in-between-green-trees-under-blue-sky-during-daytime-GYCV4cxDtmc" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In November 2025, at &lt;a href="https://www.cncf.io/kubecon-cloudnativecon-events/" rel="noopener noreferrer"&gt;KubeCon&lt;/a&gt; in Atlanta, we showed &lt;a href="https://xcp-ng.org/blog/2025/11/13/xcp-ng-on-arm-with-ampere/" rel="noopener noreferrer"&gt;XCP-ng on Ampere&lt;/a&gt;: an &lt;a href="https://amperecomputing.com/products/processors" rel="noopener noreferrer"&gt;Ampere Altra&lt;/a&gt;-based System76 Thelio Astra running XCP-ng, managed by Xen Orchestra. A real Arm machine, doing real hypervisor things, in front of real people.&lt;/p&gt;

&lt;p&gt;So in October we set ourselves the same goal on RISC-V. (One new architecture a year is clearly not enough for some of us, and by "some of us" I mostly mean me.) The target was a host that Xen Orchestra can manage, and a guest on it running containers: Docker first, then &lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt; with &lt;a href="https://k3s.io/" rel="noopener noreferrer"&gt;K3s&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Did we get there?&lt;/p&gt;

&lt;p&gt;In QEMU, yes. Xen Orchestra lists the RISC-V host and its VMs, opens their consoles, shows their stats, starts and stops them. A &lt;a href="https://www.debian.org/" rel="noopener noreferrer"&gt;Debian&lt;/a&gt; guest on that host runs Docker, and K3s reports &lt;code&gt;Ready&lt;/code&gt; about a minute after starting. And as a bonus, the same guest shows an &lt;a href="https://www.xfce.org/" rel="noopener noreferrer"&gt;XFCE&lt;/a&gt; desktop in Xen Orchestra’s console tab.&lt;/p&gt;

&lt;p&gt;Now, before anyone gets excited and starts shopping for boards: unlike the Arm demo, all of this runs in QEMU, emulated on an x86 machine. Since early October, that x86 machine has been pretending to be a RISC-V machine, with a lot of dedication and very little speed. We have not run this full stack on a RISC-V board yet. This is a prototype, research and development in the wild, and what follows is a progress report rather than an announcement (I’m repeating that to myself as much as to you, because my enthusiasm has a habit of slipping its leash).&lt;/p&gt;

&lt;h1&gt;
  
  
  The people who did the hard part first
&lt;/h1&gt;

&lt;p&gt;None of what follows exists without these people, so they come first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xcp-ng.org/blog/author/baptleduc/" rel="noopener noreferrer"&gt;Baptiste Le Duc&lt;/a&gt; and &lt;a href="https://xcp-ng.org/blog/author/oleksii/" rel="noopener noreferrer"&gt;Oleksii Kurochko&lt;/a&gt; have been porting the Xen hypervisor itself to RISC-V for a while now, and the xcp-ng.org blog has followed them: Xen &lt;a href="https://xcp-ng.org/blog/2026/09/03/xen-on-risc-v-just-ran-on-real-hardware/" rel="noopener noreferrer"&gt;booted a guest on real RISC-V hardware&lt;/a&gt; in September. Then Zheng Zhang (&lt;a href="http://english.is.cas.cn/" rel="noopener noreferrer"&gt;ISCAS&lt;/a&gt;, the Institute of Software, Chinese Academy of Sciences) took those patches and booted Xen on his own &lt;a href="https://xcp-ng.org/blog/2026/09/29/xen-on-riscv-boots-on-the-spacemit-k3-our-first-rva23-board/" rel="noopener noreferrer"&gt;SpacemiT K3&lt;/a&gt;, the first &lt;a href="https://github.com/riscv/riscv-profiles/blob/main/src/rva23-profile.adoc" rel="noopener noreferrer"&gt;RVA23&lt;/a&gt; board Xen runs on.&lt;/p&gt;

&lt;p&gt;The original version of this story on xcp-ng.org was written together with Baptiste, &lt;a href="https://xcp-ng.org/blog/author/yann-sionneau/" rel="noopener noreferrer"&gt;Yann Sionneau&lt;/a&gt; and &lt;a href="https://xcp-ng.org/blog/author/julian/" rel="noopener noreferrer"&gt;Julian Vetter&lt;/a&gt;, so their names belong up here too.&lt;/p&gt;

&lt;p&gt;So where does my October fit in?&lt;/p&gt;

&lt;p&gt;A hypervisor is not a virtualization platform. XCP-ng is Xen plus a &lt;a href="https://wiki.xenproject.org/wiki/Dom0" rel="noopener noreferrer"&gt;dom0&lt;/a&gt; with its whole package set, plus &lt;a href="https://xapi-project.github.io/" rel="noopener noreferrer"&gt;XAPI&lt;/a&gt;, the toolstack that Xen Orchestra talks to. Baptiste’s trees already carry the RISC-V Xen, the RISC-V toolstack (XAPI, &lt;a href="https://xapi-project.github.io/new-docs/xenopsd/index.html" rel="noopener noreferrer"&gt;xenopsd&lt;/a&gt;, xenguest), the dom0 kernel, and the dom0 image build. Since early October, we have been working on the layer around them: package that stack for riscv64, boot it, fix what breaks along the way, and get Xen Orchestra to manage the result. The unglamorous plumbing, in other words, which happens to be my favourite kind.&lt;/p&gt;

&lt;p&gt;A word on method, because I’d rather say it now than have someone find out later: I built much of this with heavy help from AI coding assistants, so expect parts of it to change as the patches go through upstream review.&lt;/p&gt;

&lt;h1&gt;
  
  
  What works today
&lt;/h1&gt;

&lt;p&gt;The stack runs in QEMU with 3 harts and 5 GiB of RAM. The dom0 is &lt;a href="https://wiki.almalinux.org/development/almalinux-os-kitten-10.html" rel="noopener noreferrer"&gt;AlmaLinux Kitten&lt;/a&gt; running the riscv64 &lt;a href="https://en.wikipedia.org/wiki/RPM_Package_Manager" rel="noopener noreferrer"&gt;RPMs&lt;/a&gt; of our latest build round, round 19, Xen and the dom0 kernel included. Xen Orchestra 6.9.1 is a riscv64 build of its own (&lt;a href="https://github.com/gounthar/xen-orchestra-riscv64" rel="noopener noreferrer"&gt;gounthar/xen-orchestra-riscv64&lt;/a&gt;). On October 20th, on that round, it gave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the host and its VMs in the inventory&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;host and VM consoles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;host and VM stats (the graphs are in the XO 5 interface, which XO 6 links to)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;start, clean shutdown, force shutdown, and start again&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a Debian 13 (trixie) guest with a disk on a &lt;a href="https://docs.xcp-ng.org/storage/" rel="noopener noreferrer"&gt;storage repository&lt;/a&gt; and a network that reaches the internet&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;an XFCE desktop in the VM’s console tab&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That fourth line looks boring. It wasn’t, and I’ll come back to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F01-xo6-riscv-vm-boot-console.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F01-xo6-riscv-vm-boot-console.png" alt="Xen Orchestra 6 console tab for the VM riscv1-trixie, showing systemd boot messages including the Xen guest agent service, ending at the prompt &amp;quot;Debian GNU/Linux 13 riscv1-trixie hvc0&amp;quot; and &amp;quot;riscv1-trixie login:&amp;quot;." width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. A Debian 13 RISC-V guest booting to a login prompt, in the Xen Orchestra 6 console.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F02-xo5-riscv-vm-stats.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F02-xo5-riscv-vm-stats.png" alt="Xen Orchestra 5 Stats tab for the VM riscv1-trixie on host xcp-ng-riscv64, with four charts over ten minutes: CPU usage, memory usage rising to about 700 MiB, network throughput for two interfaces, and disk read and write throughput." width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. CPU, memory, network, and disk statistics for the same guest, in Xen Orchestra 5.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Inside that guest, on a kernel built with the container options, Docker runs riscv64 containers and K3s reports &lt;code&gt;Ready&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Is it polished? Not quite. On one start, Xen Orchestra showed no IP address for the guest for more than five minutes, although the guest had its lease after 24 seconds. Xen Orchestra reports the guest’s disk as 1 GiB when the guest sees 8 GiB (a rounding error of a mere factor of eight), and Docker pulls sometimes need a retry.&lt;/p&gt;

&lt;p&gt;The packages are public, as experimental and unsigned releases in &lt;a href="https://github.com/gounthar/xcpng-riscv64-rpms" rel="noopener noreferrer"&gt;gounthar/xcpng-riscv64-rpms&lt;/a&gt;: 19 build rounds so far, published from round 10. They come from &lt;a href="https://xcp-ng.org/blog/author/yann/" rel="noopener noreferrer"&gt;Yann Dirson&lt;/a&gt;'s &lt;a href="https://github.com/xcp-ng/meta-xcpng" rel="noopener noreferrer"&gt;meta-xcpng&lt;/a&gt; &lt;a href="https://docs.yoctoproject.org/bitbake/" rel="noopener noreferrer"&gt;BitBake&lt;/a&gt; layer, with a riscv64 port of ours on top that is not part of his layer. (Yann Dirson, not to be confused with Yann Sionneau above. Yes, there are two Yanns in this story. No, it doesn’t get easier.)&lt;/p&gt;

&lt;p&gt;In a riscv64 AlmaLinux Kitten container, with the Kitten repositories and the latest round, this resolves and installs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;xen-hypervisor xen-tools kernel-xcpng-riscv64 xcp-networkd xcp-ng-release-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me be careful about what that means, because it’s easy to read too much into a green &lt;code&gt;dnf&lt;/code&gt; run. It proves the packaging, not that the stack runs. And like the Xen Orchestra build, these packages are a personal, unsupported, and not production-ready project. Please don’t put them anywhere near anything you care about.&lt;/p&gt;

&lt;h1&gt;
  
  
  Starting a VM twice in one boot
&lt;/h1&gt;

&lt;p&gt;Remember that boring-looking line about "start again"?&lt;/p&gt;

&lt;p&gt;For the first few days, we could start exactly one VM per boot. Shut it down, and the next start never ran. One VM per boot is not so much a virtualization platform as a very elaborate bootloader.&lt;/p&gt;

&lt;p&gt;The dead guest was never freed, and it took a fix on each side. In Xen, the RISC-V functions that release a dead domain’s resources were still stubs, and foreign mappings never dropped the page reference they take. In the dom0 kernel, the function that should unmap a guest’s pages returned success without telling Xen anything, so about 28 MB of every dead guest stayed pinned. (A function that reports success without doing the work: we’ve all worked with one, and some of us have written one.)&lt;/p&gt;

&lt;p&gt;With both fixes, we ran three start, shutdown, start cycles in a row, and free memory came back to its baseline each time. Both fixes are merge requests on Baptiste’s trees, waiting for his review.&lt;/p&gt;

&lt;h1&gt;
  
  
  Getting the guest on the network
&lt;/h1&gt;

&lt;p&gt;No network, no container images, so this one stood squarely between me and Docker. The first networked guest gave up right away:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grant table add_to_physmap failed, err=-38
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;-38&lt;/code&gt; is &lt;code&gt;ENOSYS&lt;/code&gt; (the kernel’s polite way of saying "never heard of it"). Xen on RISC-V did not yet implement the call a guest uses to grow its &lt;a href="https://xenbits.xen.org/docs/unstable/misc/grant-tables.txt" rel="noopener noreferrer"&gt;grant table&lt;/a&gt;, and a network frontend needs 512 grants to start. The Arm port has that case; RISC-V did not.&lt;/p&gt;

&lt;p&gt;We expected one patch and needed three: the missing case in Xen, a scratch page for grant seeding in libxenguest, and an init function in the RISC-V Linux port that returned an error and stopped grant table setup halfway. (Expect one, get three. If you’ve done any porting work, you’re nodding right now.) After those, the guest was able to ping the dom0, and later reached the internet. These are also merge requests on Baptiste’s trees, waiting for review.&lt;/p&gt;

&lt;h1&gt;
  
  
  A desktop in the console tab
&lt;/h1&gt;

&lt;p&gt;Both consoles in Xen Orchestra (XO) were text-only at first. Perfectly respectable, and perfectly black and white.&lt;/p&gt;

&lt;p&gt;Julian Vetter had already solved this for the Arm port, so we borrowed his approach: QEMU runs in dom0 only as a Xen &lt;a href="https://wiki.xenproject.org/wiki/Paravirtualization_%28PV%29" rel="noopener noreferrer"&gt;PV backend&lt;/a&gt;, the guest uses Xen’s PV framebuffer, and Xen Orchestra connects to it like any other console.&lt;/p&gt;

&lt;p&gt;On RISC-V that meant building a riscv64 QEMU from his XCP-ng QEMU branch plus two RISC-V patches, teaching xenopsd’s RISC-V builder to start it, and enabling the keyboard and mouse frontend in the guest kernel. Then one setting on the VM:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;platform:vfb=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And XFCE shows up in the console tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F03-xo6-riscv-desktop-console.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F03-xo6-riscv-desktop-console.png" alt="Xen Orchestra 6 showing the VM riscv2-desktop in the XCP-ng on RISC-V pool. The Console tab displays an XFCE desktop with a terminal where uname -a prints a Linux 6.18 riscv64 kernel." width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. An XFCE desktop on a RISC-V guest, in the Xen Orchestra 6 console tab. &lt;code&gt;uname -a&lt;/code&gt; reports riscv64.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I know a desktop is not what anyone runs a hypervisor for. It does make a better demo than a login prompt.&lt;/p&gt;

&lt;p&gt;And once there’s a desktop, you know what has to run on it. On October 6th, &lt;a href="https://www.chocolate-doom.org/" rel="noopener noreferrer"&gt;Chocolate Doom&lt;/a&gt; 3.1.0 ran in that same console tab, with the &lt;a href="https://freedoom.github.io/" rel="noopener noreferrer"&gt;Freedoom&lt;/a&gt; Phase 1 game data (not id’s WAD), both straight from Debian trixie’s riscv64 repositories. Installing the two packages took 6 minutes 39 seconds under emulation.&lt;/p&gt;

&lt;p&gt;The console’s first trick was to sit on its loading logo forever. Xen Orchestra reaches a VM’s console through XAPI’s &lt;a href="https://en.wikipedia.org/wiki/WebSocket" rel="noopener noreferrer"&gt;websocket&lt;/a&gt; proxy, and that proxy only accepted sockets named &lt;code&gt;vnc-N&lt;/code&gt;, while the PV framebuffer’s QEMU names its socket &lt;code&gt;pv-vnc-N&lt;/code&gt;. Once XAPI accepted that name too (build round 13), the logo gave way to the desktop. Then the mouse pointer kept drifting to the right, because QEMU doesn’t tell the guest’s virtual keyboard driver how big the screen is. Telling the guest kernel directly fixed it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xen_kbdfront.ptr_size=1280,800
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F04-xo6-riscv-doom-console.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F10%2F23%2F04-xo6-riscv-doom-console.png" alt="Xen Orchestra 6 Console tab for the VM riscv2-desktop, showing an XFCE desktop with a Chocolate Doom 3.1.0 window running Freedoom Phase 1." width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4. Freedoom on a RISC-V guest, in the Xen Orchestra 6 console tab, on October 6th, on an earlier build round.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same run on video, recorded on 2026-10-06.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Is it playable? I honestly don’t know: nobody measured the frame rate, and under emulation I wouldn’t bet on it. But it runs, and some traditions have to be respected.&lt;/p&gt;

&lt;h1&gt;
  
  
  One fix that came from upstream
&lt;/h1&gt;

&lt;p&gt;For a while, Xen Orchestra would lose the RISC-V pool every time XAPI was restarted, and only a restart of xo-server would bring it back.&lt;/p&gt;

&lt;p&gt;That one, we didn’t have to fix ourselves: Xen Orchestra 6.9.0 fixed it. Since we updated, xo-server reconnects on its own, about 20 seconds after a XAPI restart. (I’d love to take credit for that one. I can’t. All I did was update.)&lt;/p&gt;

&lt;h1&gt;
  
  
  What emulation costs
&lt;/h1&gt;

&lt;p&gt;QEMU emulating RISC-V on x86 is slow, and it shows.&lt;/p&gt;

&lt;p&gt;That minute for K3s is emulation time. A guest reads its disk at about 800 KB/s. Yes, kilobytes, per second, and I’ll let that sink in slowly, at roughly the speed of the disk. A clean shutdown takes about 20 seconds. The packages are built under emulation too: the &lt;a href="https://ocaml.org/" rel="noopener noreferrer"&gt;OCaml&lt;/a&gt; build alone takes 72 minutes (plenty of time to reconsider a few life choices).&lt;/p&gt;

&lt;p&gt;None of those figures say anything about how RISC-V hardware performs. They only explain why we want to switch to using a real hardware board.&lt;/p&gt;

&lt;h1&gt;
  
  
  What’s next
&lt;/h1&gt;

&lt;p&gt;Three directions, none of them dated (on purpose: a date in a blog post reads like a promise, and this is a prototype):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real RISC-V hardware.&lt;/strong&gt; Our boot path still assumes QEMU’s virtual machine layout. Booting the full stack on a board, as our Arm demo did on Ampere, is the next milestone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Upstreaming.&lt;/strong&gt; Our patches go to Baptiste’s Xen, Linux, and toolstack trees first, for review, then upstream from there. Some are merged in his trees; most are still waiting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reproducible package builds.&lt;/strong&gt; Native builds on a RISC-V board instead of emulation, and a riscv64 port that fits meta-xcpng cleanly.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  How to help
&lt;/h1&gt;

&lt;p&gt;Don’t have a RISC-V board? This stack hasn’t met one yet either, so you’re in good company.&lt;/p&gt;

&lt;p&gt;You don’t need one to contribute. Start with the packages: in a riscv64 AlmaLinux Kitten container, download the latest round and install it as a local repository, as the &lt;a href="https://github.com/gounthar/xcpng-riscv64-rpms" rel="noopener noreferrer"&gt;repository’s README&lt;/a&gt; shows, and run the install command above. That only proves they install, but a dependency that does not resolve or a file in the wrong place is a bug, and telling us about it on the &lt;a href="https://xcp-ng.org/forum/" rel="noopener noreferrer"&gt;forum&lt;/a&gt; is already a contribution.&lt;/p&gt;

&lt;p&gt;If you know RISC-V, virtualization, or packaging, you are welcome to review Baptiste’s &lt;a href="https://gitlab.com/xen-project/people/baptleduc/xen" rel="noopener noreferrer"&gt;Xen tree&lt;/a&gt;, &lt;a href="https://github.com/baptleduc/linux-xen-riscv" rel="noopener noreferrer"&gt;Linux tree&lt;/a&gt;, and &lt;a href="https://github.com/baptleduc/xen-api" rel="noopener noreferrer"&gt;xen-api fork&lt;/a&gt;. (Yes, this is me asking for reviewers again. I’m told persistence is a feature.)&lt;/p&gt;

&lt;h1&gt;
  
  
  What I’m taking away from October
&lt;/h1&gt;

&lt;p&gt;An emulator number is an emulator number: 800 KB/s disk reads say something about QEMU on x86 and nothing about RISC-V silicon. When RISC-V is missing something, look at the Arm port first, because the grant table case was already there, and so was Julian’s console approach. Budget for three patches when you expect one. Update before you debug, since the XAPI reconnect problem had been fixed in Xen Orchestra 6.9.0 all along, waiting for me to notice. And a package that installs is not a stack that runs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is my personal retelling of a post published on the XCP-ng blog, written with a bit more mischief than the house voice allows. The original, co-authored with Baptiste Le Duc, Yann Sionneau and Julian Vetter, lives on xcp-ng.org: &lt;a href="https://xcp-ng.org/blog/2026/10/23/xcp-ng-and-xen-orchestra-on-risc-v-a-progress-report-from-the-emulator/" rel="noopener noreferrer"&gt;XCP-ng and Xen Orchestra on RISC-V: a progress report from the emulator&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>riscv</category>
      <category>xcpng</category>
      <category>xenorchestra</category>
      <category>xen</category>
    </item>
    <item>
      <title>Xen Summit 2026 in Munich: RISC-V takes the graveyard slot and wakes the room</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Mon, 28 Sep 2026 18:39:42 +0000</pubDate>
      <link>https://dev.to/gounthar/xen-summit-2026-in-munich-risc-v-takes-the-graveyard-slot-and-wakes-the-room-3nk6</link>
      <guid>https://dev.to/gounthar/xen-summit-2026-in-munich-risc-v-takes-the-graveyard-slot-and-wakes-the-room-3nk6</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on September 28, 2026 at &lt;a href="https://bruno.verachten.fr/2026/09/28/xen-summit-2026-in-munich-risc-v-takes-the-graveyard-slot-and-wakes-the-room/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2026/09/28/xen-summit-2026-in-munich-risc-v-takes-the-graveyard-slot-and-wakes-the-room/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F09%2F28%2F00-header-munich-neues-rathaus.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F09%2F28%2F00-header-munich-neues-rathaus.jpg" alt="The Neues Rathaus on Marienplatz in Munich, its clock tower and Gothic spires lit gold by the evening sun against a clear blue sky" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every September, part of the Xen community does something slightly radical: it stops talking through mailing lists and &lt;a href="https://matrix.org/" rel="noopener noreferrer"&gt;Matrix&lt;/a&gt; handles and spends three days in the same room. (Faces! Attached to the handles! It never stops being a little strange.) This year the room was in Munich, hosted by &lt;a href="https://www.renesas.com/" rel="noopener noreferrer"&gt;Renesas&lt;/a&gt;, from 15 to 17 September. Fourteen of us from &lt;a href="https://vates.tech/" rel="noopener noreferrer"&gt;Vates&lt;/a&gt; made the trip, more colleagues followed remotely, and we gave three talks.&lt;/p&gt;

&lt;p&gt;Here’s what we said, what I heard, and why I came back more excited about RISC-V than I left. Which, if you know me, is quite a feat. The needle was already pretty far into the red.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why leave the comfort of the mailing list?
&lt;/h1&gt;

&lt;p&gt;Most of our work with the wider Xen community happens in writing, and that works. Honestly, it does. But some topics would take weeks to settle in a chat thread, and some would never come up at all, because nobody would think to raise them in the middle of a normal working day, somewhere between two rebases. Put the same people in one place for three days, and those topics surface on their own, over coffee, in a corridor, or at dinner. No &lt;code&gt;git send-email&lt;/code&gt; required.&lt;/p&gt;

&lt;p&gt;Vates was one of the sponsors, but the &lt;a href="https://xensummit2026.sched.com/" rel="noopener noreferrer"&gt;Summit&lt;/a&gt; belongs to the Xen community, not to us. (Sponsoring an event doesn’t buy you the room, and that’s precisely what makes the room worth being in.) Silicon vendors, consultancies, universities, and embedded companies were all there, which made the conversations much wider than the ones we have among ourselves. Our own echo chamber has lovely acoustics, but it’s still an echo chamber.&lt;/p&gt;

&lt;h1&gt;
  
  
  Three talks walk into a summit
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;XCP-ng State of the Union&lt;/strong&gt; (me, Tuesday 10:00). A round of everything that happened since the Spring Meetup in April (five months, roughly three geological eras in our line of work): the market around us, &lt;a href="https://xcp-ng.org/" rel="noopener noreferrer"&gt;XCP-ng&lt;/a&gt; 8.3 LTS and &lt;a href="https://en.wikipedia.org/wiki/Qcow" rel="noopener noreferrer"&gt;QCOW2&lt;/a&gt;, &lt;a href="https://xen-orchestra.com/" rel="noopener noreferrer"&gt;Xen Orchestra&lt;/a&gt; 6, the DevOps tooling, then Arm and RISC-V.&lt;/p&gt;

&lt;p&gt;One figure I was very happy to put on screen: Vates authored 104 commits to Xen 4.22, 10.9% of the release, making us its third-largest contributor, and more than half of those commits were RISC-V work. You can probably guess which part of that sentence made me happiest.&lt;/p&gt;

&lt;p&gt;The RISC-V slide ended with a direct ask to the room: come and review the series, because more reviewers move it faster. Yes, I stood in front of a room full of reviewers and asked for reviewers. Subtle, I am not. Several people came to find me afterwards (more on that below, and it’s the good kind of afterwards).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XCP-ng+Xen status update&lt;/strong&gt; (Teddy Astie, Tuesday 13:30). Teddy walked through the Xen work behind XCP-ng, and his list is long enough that I’ll give it to you as a list rather than as one heroic sentence (the first draft of this post tried the heroic sentence; the sentence won):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;general x86 improvements, including work in progress on guests with more than 128 vCPUs;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Arm for cloud, where &lt;a href="https://xapi-project.github.io/" rel="noopener noreferrer"&gt;XAPI&lt;/a&gt; support is partly working and some of it is already upstream;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;confidential guests with &lt;a href="https://www.amd.com/en/developer/sev.html" rel="noopener noreferrer"&gt;AMD SEV&lt;/a&gt;, which work experimentally, with patches on the mailing list where help is welcome;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PV-IOMMU;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and GPU virtualisation, through both &lt;a href="https://www.qemu.org/docs/master/system/devices/virtio/virtio-gpu.html" rel="noopener noreferrer"&gt;virtio-gpu&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Single-root_input/output_virtualization" rel="noopener noreferrer"&gt;SR-IOV&lt;/a&gt; vGPUs on AMD and Intel cards.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a lot of plates spinning. His &lt;a href="https://hosted-files.sched.co/xensummit2026/5f/slides.pdf" rel="noopener noreferrer"&gt;slides&lt;/a&gt; are on the Summit schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Xen on RISC-V: From Dom0less to xl&lt;/strong&gt; (Baptiste Le Duc and Oleksii Kurochko, Wednesday 17:00). This is the talk I keep telling people about (whether they asked or not), so it gets its own section.&lt;/p&gt;

&lt;h1&gt;
  
  
  The graveyard slot, resurrected
&lt;/h1&gt;

&lt;p&gt;Anyone who has spoken at a conference knows the last slot of day two. The shared dinner is next, the room is tired, and half the audience is already thinking about beer. In Munich. You can hardly blame them.&lt;/p&gt;

&lt;p&gt;Baptiste and Oleksii got the room back anyway.&lt;/p&gt;

&lt;p&gt;Baptiste has a sense of showmanship, and the enthusiasm, the stubbornness, and the plain joy the two of them put into this work came through in every slide. The audience applauded in the middle of the talk, more than once. Mid-talk applause, at 17:00 on day two, from hypervisor people. Let that sink in for a second.&lt;/p&gt;

&lt;p&gt;And that audience matters. These were committers and maintainers, people who already follow the RISC-V port on the mailing list and know every patch series by name (and, I suspect, by version number). For them, the talk turned Xen on RISC-V from something ethereal into something concrete. From vaporware to hardware, if you’ll forgive me.&lt;/p&gt;

&lt;p&gt;The title says the plan, and the plan has two lanes. Upstream, the port goes &lt;a href="https://xenbits.xen.org/docs/unstable/features/dom0less.html" rel="noopener noreferrer"&gt;dom0less&lt;/a&gt; first: Xen builds its guests itself at boot, from a static configuration. Downstream, Baptiste and Oleksii already run guests with several vCPUs, on &lt;a href="https://www.qemu.org/" rel="noopener noreferrer"&gt;QEMU&lt;/a&gt; and on real hardware: a &lt;a href="https://www.sifive.com/boards/hifive-premier-p550" rel="noopener noreferrer"&gt;HiFive Premier P550&lt;/a&gt;, and a &lt;a href="https://www.spacemit.com/en/" rel="noopener noreferrer"&gt;SpacemiT&lt;/a&gt; K3 that a community contributor booted independently.&lt;/p&gt;

&lt;p&gt;Then comes a full dom0 with the &lt;a href="https://xenbits.xen.org/docs/unstable/man/xl.1.html" rel="noopener noreferrer"&gt;xl&lt;/a&gt; toolstack, and that part works too: &lt;code&gt;xl info&lt;/code&gt;, &lt;code&gt;xl list&lt;/code&gt;, &lt;code&gt;xl create&lt;/code&gt; and &lt;code&gt;xl console&lt;/code&gt; are all there. Four commands. Doesn’t sound like much? If you’ve typed them on x86 without a second thought, try picturing the work behind each one on a young architecture.&lt;/p&gt;

&lt;p&gt;They ended with a demo, pre-recorded in case the conference network let them down, creating a guest from dom0 and talking to it through its console. Is pre-recording cheating? No. That’s experience. Anyone who has ever bet a live demo on conference Wi-Fi knows exactly why, and the knowledge is rarely acquired cheaply.&lt;/p&gt;

&lt;p&gt;Baptiste wrote up the P550 story in &lt;a href="https://xcp-ng.org/blog/2026/09/03/xen-on-risc-v-just-ran-on-real-hardware/" rel="noopener noreferrer"&gt;Xen on RISC-V just ran on real hardware!&lt;/a&gt;, including why a core designed before the hypervisor extension was ratified made it harder than it sounds (and "harder than it sounds" is doing a lot of heavy lifting in that sentence). Their &lt;a href="https://hosted-files.sched.co/xensummit2026/5c/xen-riscv-from-dom0less-to-xl.pdf" rel="noopener noreferrer"&gt;slides&lt;/a&gt; are on the Summit schedule.&lt;/p&gt;

&lt;h1&gt;
  
  
  RVA23: a profile, and not the LinkedIn kind
&lt;/h1&gt;

&lt;p&gt;For years, RISC-V meant small boards for hobbyists and embedded work. That is changing, and the reason isn’t a chip. It’s a profile.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/riscv/riscv-profiles/blob/main/src/rva23-profile.adoc" rel="noopener noreferrer"&gt;RVA23&lt;/a&gt; is a ratified set of mandatory extensions, and the hypervisor extension is one of them. An RVA23 chip is not automatically fast enough to be a server, but it has what virtualisation needs, in a form software can rely on. Once that baseline exists, hardware built for servers can follow, and a server that can’t run a hypervisor is not much use in a datacenter (a very expensive space heater, at best).&lt;/p&gt;

&lt;p&gt;So the real question for Xen is whether a &lt;a href="https://en.wikipedia.org/wiki/Hypervisor" rel="noopener noreferrer"&gt;type-1 hypervisor&lt;/a&gt; will be ready when that hardware arrives. Showing up to the party after the cake is gone is never a great look. That is what Baptiste and Oleksii are working on: making sure we’re there before the candles are lit.&lt;/p&gt;

&lt;h1&gt;
  
  
  The hallway track: no slides, all signal
&lt;/h1&gt;

&lt;p&gt;The corridor conversations were at least as exciting as the talks. No slides, no timer, nobody waving a "five minutes left" sign at you.&lt;/p&gt;

&lt;p&gt;Several people came over after my talk because, as one of them put it, they had noticed my "unbridled enthusiasm" for RISC-V. Fair enough. Guilty as charged. I’m not sure anyone has ever managed to put a bridle on it, least of all me. From those conversations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;a university lab that runs real RISC-V hardware in the loop for its CI, which is exactly what a port like ours needs;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;an embedded engineer who would love to see Xen on RISC-V in safety-critical systems one day;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and, my favourite detail, people swapping links to order RISC-V boards on the spot, because everyone is waiting for hardware.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing says "this is getting real" quite like hypervisor people doing their shopping in the corridor.&lt;/p&gt;

&lt;p&gt;The atmosphere was friendly and generous, with the kind of good-natured competition where everyone leaves wanting to push a bit harder. Friendly rivalry: the renewable energy of open source.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why I care (and why the bridle stays off)
&lt;/h1&gt;

&lt;p&gt;I’m not a visionary. But I do have a decent memory, and I’ve seen this pattern before: about fifteen years ago, with Arm, a new architecture went from "interesting" to "real" in a few years, and the people who had their software ready early were the ones who mattered. I think RISC-V can go further than Arm did, because the &lt;a href="https://en.wikipedia.org/wiki/Instruction_set_architecture" rel="noopener noreferrer"&gt;ISA&lt;/a&gt; itself is open. Open ISA, open hypervisor: I do like the symmetry.&lt;/p&gt;

&lt;p&gt;Is getting ready early a gamble? Sure.&lt;/p&gt;

&lt;p&gt;Call it a calculated RISC.&lt;/p&gt;

&lt;p&gt;There are enough signals now to say: let’s go. Or, since we were in Munich, &lt;em&gt;los geht’s&lt;/em&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  What’s next
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The talk recordings aren’t out yet; they’ll be linked here once the &lt;a href="https://xenproject.org/" rel="noopener noreferrer"&gt;Xen Project&lt;/a&gt; publishes them. (Refresh responsibly.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Baptiste and Oleksii’s work deserves more than a section in a recap, and a dedicated post is on the way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you want to follow the RISC-V port, or answer that call for reviewers (yes, I’m asking again; persistence is a feature, not a bug), the patch series are on the &lt;a href="https://lists.xenproject.org/cgi-bin/mailman/listinfo/xen-devel" rel="noopener noreferrer"&gt;xen-devel mailing list&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to the Xen Project team and to Renesas for organising the Summit, and to everyone who came to talk to us, enthusiasm bridled or not.&lt;/p&gt;

</description>
      <category>xen</category>
      <category>xensummit</category>
      <category>riscv</category>
      <category>xcpng</category>
    </item>
    <item>
      <title>yum said 301, the machine said 2024</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Wed, 29 Jul 2026 16:18:12 +0000</pubDate>
      <link>https://dev.to/gounthar/yum-said-301-the-machine-said-2024-1kd8</link>
      <guid>https://dev.to/gounthar/yum-said-301-the-machine-said-2024-1kd8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on July 29, 2026 at &lt;a href="https://bruno.verachten.fr/2026/07/29/yum-said-301-the-machine-said-2024/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2026/07/29/yum-said-301-the-machine-said-2024/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I sat down to put the July batch of updates on the &lt;a href="https://en.wikipedia.org/wiki/XCP-ng" rel="noopener noreferrer"&gt;XCP-ng&lt;/a&gt; 8.3 host in my home lab, which is normally a thirty second job with a coffee in the other hand, and got this instead:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://mirrors.xcp-ng.org/8/8.3/base/x86_64/repodata/repomd.xml: [Errno 14] HTTPS Error 301 - Moved Permanently
Trying other mirror.
...
failure: repodata/repomd.xml from xcp-ng-base: [Errno 256] No more mirrors to try.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Read that as an operator and there is exactly one conclusion on offer. The mirrors moved, the repo files are stale, someone upstream forgot to update something. It is the only story that message tells, and it tells it with real confidence.&lt;/p&gt;

&lt;p&gt;So I did what the message told me to do. I opened &lt;code&gt;/etc/yum.repos.d/xcp-ng.repo&lt;/code&gt; and stared at the URLs. I ran &lt;code&gt;yum clean all&lt;/code&gt;. I ran &lt;code&gt;rm -rf /var/cache/yum&lt;/code&gt; for good measure. I was one text editor away from pinning a mirror by hand and bypassing whatever was broken instead of understanding it.&lt;/p&gt;

&lt;p&gt;None of it helped, and it cost me the better part of an afternoon, because every single word of that error message is about the wrong thing.&lt;/p&gt;

&lt;p&gt;The host did not know what day it was.&lt;/p&gt;

&lt;h1&gt;
  
  
  The redirect is real, and it is completely fine
&lt;/h1&gt;

&lt;p&gt;The first thing worth doing is proving the mirrors innocent, since that is where the message points. Following the chain by hand with &lt;a href="https://en.wikipedia.org/wiki/CURL" rel="noopener noreferrer"&gt;curl&lt;/a&gt;, from my laptop rather than from the host that was failing, which matters more than it sounds like it does and which I will come back to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sIL&lt;/span&gt; http://mirrors.xcp-ng.org/8/8.3/base/x86_64/repodata/repomd.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://mirrors.xcp-ng.org/...          301 -&amp;gt; https://mirrors.xcp-ng.org/...
https://mirrors.xcp-ng.org/...         302 -&amp;gt; https://xcpng-mirror.as208069.net/...
https://xcpng-mirror.as208069.net/...  200, 3117 bytes, application/xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Two hops and then the file. The first hop is the plain &lt;a href="https://en.wikipedia.org/wiki/HTTP_301" rel="noopener noreferrer"&gt;301&lt;/a&gt; that pushes you from HTTP to HTTPS, which every sane public mirror does now. The second is &lt;a href="https://github.com/etix/mirrorbits" rel="noopener noreferrer"&gt;MirrorBits&lt;/a&gt; picking a mirror near you, and you can watch it think out loud in the headers it emits on the way past:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;link: &amp;lt;https://xcpng.reloumirrors.net/...&amp;gt;; rel=duplicate; pri=1; geo=fr
link: &amp;lt;https://rg2-xcpng-mirror.reptigo.fr/...&amp;gt;; rel=duplicate; pri=2; geo=fr
link: &amp;lt;https://xcpng-mirror.as208069.net/...&amp;gt;; rel=duplicate; pri=3; geo=fr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That is a mirror network doing precisely what it is designed to do, ranking candidates by priority and by my geography, and handing me one. &lt;code&gt;curl -L&lt;/code&gt; walks the whole thing and comes out the other side with a 200 and a valid XML file. Nothing moved. Nothing is down.&lt;/p&gt;

&lt;p&gt;Which means the 301 in that error message is not a fault report at all. It is a description of normal, healthy infrastructure, printed at the exact moment something completely different fell over.&lt;/p&gt;

&lt;h1&gt;
  
  
  Two boxes, one command, one variable
&lt;/h1&gt;

&lt;p&gt;There is a much cheaper version of that check, and if you have a second machine you should run it before you touch anything else. I have another host on the same LAN, same switch, same egress, same repo files. I ran the same update there.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upgrade  28 Packages
Total download size: 103 M
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;xcp-ng-release 8.3.0-38&lt;/code&gt;, &lt;code&gt;xen-hypervisor 4.17.6-9.3.1.xcpng8.3&lt;/code&gt;, the whole &lt;code&gt;26.1.11-1.3&lt;/code&gt; XAPI stack, straight down from &lt;code&gt;xcp-ng-updates&lt;/code&gt; without a murmur.&lt;/p&gt;

&lt;p&gt;One command, and in one go I had eliminated a mirror outage, a bad repo file shipped upstream, a broken batch, DNS, my router, and my ISP. All of it, gone from the list. Whatever was wrong lived on the one host, and I had wasted my afternoon looking outward when the control test was sitting there in the next room.&lt;/p&gt;

&lt;p&gt;I keep relearning that lesson and I keep being surprised by it, so I am writing it down this time.&lt;/p&gt;

&lt;h1&gt;
  
  
  The certificate had a start date
&lt;/h1&gt;

&lt;p&gt;Once you know the fault is local, the next question is what the failing host does differently from the working one, and the answer turned out to be about &lt;a href="https://en.wikipedia.org/wiki/Transport_Layer_Security" rel="noopener noreferrer"&gt;TLS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The mirrors use &lt;a href="https://en.wikipedia.org/wiki/Let%27s_Encrypt" rel="noopener noreferrer"&gt;Let’s Encrypt&lt;/a&gt; certificates on a normal ninety day cycle. &lt;code&gt;mirrors.xcp-ng.org&lt;/code&gt; and &lt;code&gt;updates.xcp-ng.org&lt;/code&gt; were both valid from 2026-05-31 to 2026-08-29, and the mirror I was actually landing on, &lt;code&gt;xcpng-mirror.as208069.net&lt;/code&gt;, from 2026-07-01 to 2026-09-29. All of them comfortably current.&lt;/p&gt;

&lt;p&gt;Here is the bit that I think trips everybody, including me. An &lt;a href="https://en.wikipedia.org/wiki/X.509" rel="noopener noreferrer"&gt;X.509 certificate&lt;/a&gt; does not just have an expiry date. It has a &lt;code&gt;notBefore&lt;/code&gt; as well as a &lt;code&gt;notAfter&lt;/code&gt;, and a host that believes it is living before that &lt;code&gt;notBefore&lt;/code&gt; will refuse the handshake with &lt;code&gt;certificate is not yet valid&lt;/code&gt;. That is a sentence most of us have simply never had to read, because in fifteen years of operations the failure has always run the other way round. Certificates expire. They do not un-expire.&lt;/p&gt;

&lt;p&gt;Then I ran the least imaginative command available on the failing host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sun Sep  8 10:49:04 CEST 2024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;September 2024. Twenty two months before the certificate it was being asked to trust had even been issued. The other host, checked in the same minute, was on the correct date with chrony synced to within 46 microseconds of its upstream.&lt;/p&gt;

&lt;p&gt;One variable, and it was the one nobody thinks to look at.&lt;/p&gt;

&lt;p&gt;This is also why that clean curl chain earlier came off my laptop. Run the same &lt;code&gt;curl -sIL&lt;/code&gt; on the host with the 2024 clock and you do not get 301, 302, 200 and a tidy XML file. curl follows the 301 to HTTPS exactly as before, hits the same handshake yum was hitting, and stops there with &lt;code&gt;SSL certificate problem: certificate is not yet valid&lt;/code&gt;. Which is, in fairness, the truth, and rather better than what yum told me. If you are following along on the broken box and wondering why your output looks nothing like mine, that is why, and you can take it as a free confirmation of the diagnosis.&lt;/p&gt;

&lt;p&gt;And a machine you have recently installed is exactly the machine most likely to be confidently wrong about the year. A dead &lt;a href="https://en.wikipedia.org/wiki/Real-time_clock" rel="noopener noreferrer"&gt;RTC&lt;/a&gt; battery, a board that came back from a power cut on its factory default date, an isolated segment where &lt;a href="https://en.wikipedia.org/wiki/Network_Time_Protocol" rel="noopener noreferrer"&gt;NTP&lt;/a&gt; never resolved. Nothing stops you, and everything else works.&lt;/p&gt;

&lt;h1&gt;
  
  
  So why does yum lie about it?
&lt;/h1&gt;

&lt;p&gt;Because of two pieces of code in &lt;code&gt;python-urlgrabber&lt;/code&gt; that were never really meant to meet each other.&lt;/p&gt;

&lt;p&gt;yum on &lt;a href="https://wiki.xenproject.org/wiki/Dom0" rel="noopener noreferrer"&gt;dom0&lt;/a&gt; fetches through urlgrabber sitting on top of pycurl. The header callback updates the object’s idea of what it is downloading the instant it sees a &lt;code&gt;Location:&lt;/code&gt; header, which is before anything at all has been attempted at the new address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;
    &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So after that first 301, &lt;code&gt;self.scheme&lt;/code&gt; is &lt;code&gt;https&lt;/code&gt;. Perfectly reasonable so far.&lt;/p&gt;

&lt;p&gt;Then the transfer fails, and the exception handler goes to compose a message for me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_code&lt;/span&gt;          &lt;span class="c1"&gt;# getinfo(RESPONSE_CODE), still 301
&lt;/span&gt;&lt;span class="n"&gt;errstr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;pyerr2str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errcode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;Unknown&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;299&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_bytes_repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%s Error %d - %s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;errstr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;curl#%s - &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%s&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errcode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errstr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A TLS handshake that fails never gets as far as producing an HTTP response code, so &lt;code&gt;RESPONSE_CODE&lt;/code&gt; is still sitting at &lt;code&gt;301&lt;/code&gt; from the hop that succeeded. &lt;code&gt;scheme&lt;/code&gt; is now &lt;code&gt;https&lt;/code&gt; because the header callback moved it. The condition holds, the first branch fires, and &lt;code&gt;errstr&lt;/code&gt; is thrown away without ever being printed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;errstr&lt;/code&gt; was &lt;code&gt;SSL certificate problem: certificate is not yet valid&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What comes out instead is &lt;code&gt;HTTPS Error 301 - Moved Permanently&lt;/code&gt;, which is the scheme of the hop that failed, the status code of the hop that worked, and the human readable text of neither of them.&lt;/p&gt;

&lt;p&gt;That also explains a piece of folklore you will find on the forum, which is that changing the repo URL to &lt;code&gt;https://&lt;/code&gt; makes yum tell you the truth. It does, and it is worth understanding why, because it is not a fix. Point the repo straight at &lt;code&gt;https://&lt;/code&gt; and there is no redirect at all, so &lt;code&gt;RESPONSE_CODE&lt;/code&gt; is 0, the &lt;code&gt;else&lt;/code&gt; branch runs, and libcurl’s own honest message survives intact. Same failure, same host, same second. Different sentence.&lt;/p&gt;

&lt;p&gt;The general form of this is worth carrying around in your head, because it is not specific to XCP-ng or to these mirrors at all: &lt;strong&gt;in yum, any transport failure that happens after a redirect is reported as the redirect&lt;/strong&gt;. TLS, DNS, connection refused, all of it gets the same treatment. If a yum error names a 3xx, the 3xx is the last thing that went right.&lt;/p&gt;

&lt;h1&gt;
  
  
  The best part: the time daemon was working perfectly
&lt;/h1&gt;

&lt;p&gt;I could have stopped at "set NTP", fixed the clock, taken the updates and gone to bed. I am glad I did not, because the interesting part was still underneath, and on this host NTP was already on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl is-active chronyd
systemctl is-enabled chronyd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;active
enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Service running. Service enabled, so it survives reboots. Network fine, DNS fine, egress fine. Every check an operator would actually think to run comes back green, and on that evidence you would confidently conclude that time is not your problem and go and look somewhere else. Which is what I had spent the afternoon doing.&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chronyc sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;210 Number of sources = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Zero. Here is &lt;code&gt;/etc/chrony.conf&lt;/code&gt; in its entirety, with the comments stripped out:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;There is no &lt;code&gt;server&lt;/code&gt; line. There is no &lt;code&gt;pool&lt;/code&gt; line. &lt;a href="https://en.wikipedia.org/wiki/Chrony" rel="noopener noreferrer"&gt;chronyd&lt;/a&gt; had been up for a day and a half, running flawlessly, doing exactly and only what it had been told to do, which was nothing whatsoever.&lt;/p&gt;

&lt;p&gt;That is the failure mode worth naming out loud, because it is invisible from every angle except one. A time daemon with no sources is indistinguishable from a healthy time daemon unless you run &lt;code&gt;chronyc sources&lt;/code&gt;, and nobody runs &lt;code&gt;chronyc sources&lt;/code&gt; until they already suspect the clock. So the host serenely holds whatever date it was installed with, for as long as you care to leave it, and the first thing that ever tells you about it is a yum error about a redirect.&lt;/p&gt;

&lt;p&gt;My best guess at the origin is the installer’s time step, where you can either hand it NTP servers or just set the date yourself. Choosing the second produces precisely this file. It is a reasonable thing for an installer to offer and a reasonable thing to pick when you are in a hurry, and the consequence turns up months later wearing a completely unrelated costume.&lt;/p&gt;

&lt;p&gt;So the check is not "is NTP running". Running is not the question. The question is &lt;code&gt;chronyc sources&lt;/code&gt;, and if that says zero, you have found it.&lt;/p&gt;

&lt;h1&gt;
  
  
  The fix, which takes a minute
&lt;/h1&gt;

&lt;p&gt;The whole diagnostic is one command, and I want that on the record because of how long it took me to type it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the date is wrong, the next command decides which of two quite different problems you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chronyc sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that lists sources, the daemon knows where to get the time and has simply not applied it yet, and you can step the clock straight away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chronyc makestep
&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;makestep&lt;/code&gt; is the step people skip, and it matters. chrony corrects offsets by slewing the clock gently rather than yanking it, which is the right behaviour when you are drifting by milliseconds and completely useless when you are out by two years. You can sit and watch a correctly configured host stay broken indefinitely without it.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;chronyc sources&lt;/code&gt; came back with &lt;code&gt;Number of sources = 0&lt;/code&gt;, which is the case this whole post is about, then &lt;code&gt;makestep&lt;/code&gt; on its own will do exactly nothing and tell you it succeeded. There is no measured offset for it to step to, because there is nothing measuring. You have to give the daemon somewhere to look first. Put the servers into &lt;code&gt;/etc/chrony.conf&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server 0.centos.pool.ntp.org iburst
server 1.centos.pool.ntp.org iburst
server 2.centos.pool.ntp.org iburst
server 3.centos.pool.ntp.org iburst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;On an isolated network, point those at whatever local time source you have. Then restart the daemon and check that the sources actually came up before you trust anything downstream of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart chronyd
chronyc sources
chronyc makestep
&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stock config carries &lt;code&gt;makestep 1.0 3&lt;/code&gt;, so once there are real sources chronyd will usually step the clock itself within its first few updates and the explicit &lt;code&gt;makestep&lt;/code&gt; is belt to that braces. Running it costs nothing and saves you staring at &lt;code&gt;date&lt;/code&gt; wondering how long to wait.&lt;/p&gt;

&lt;p&gt;Editing the file rather than just stepping the clock is the part worth not skipping. Fix the time without fixing the config and you have fixed it precisely until the next reboot, at which point the host quietly goes back to being wrong and you get to have this afternoon all over again.&lt;/p&gt;

&lt;p&gt;Or set NTP from &lt;code&gt;xsconsole&lt;/code&gt;, which does the same thing with fewer chances to fat finger it.&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yum clean all
yum check-update
yum update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my host &lt;code&gt;check-update&lt;/code&gt; came straight back with the full 28 package July batch, and &lt;code&gt;yum update&lt;/code&gt; took the lot and exited 0, with the repository file exactly as it shipped. Total elapsed time once I actually knew the cause: about a minute.&lt;/p&gt;

&lt;p&gt;Small thing worth knowing if you are scripting any of this, and pleasingly on theme for a post about misleading exit status: &lt;code&gt;yum check-update&lt;/code&gt; returns 100 when there are updates waiting, not 0. Zero means there is nothing to do. Treat 100 as an error and you will have built yourself a second thing that reports the wrong state confidently.&lt;/p&gt;

&lt;p&gt;Which brings me to the thing I nearly did, and you probably will too. If you pinned a mirror by hand while you were flailing around, undo it and put &lt;code&gt;http://mirrors.xcp-ng.org&lt;/code&gt; back. Hand pinning opts you out of the geo selection you saw in those duplicate link headers earlier, and much more importantly it opts you out of failover on the day that one mirror has a bad afternoon. It is exactly the sort of "temporary" edit that is still sitting there two years later, on the afternoon it finally costs you something.&lt;/p&gt;

&lt;p&gt;One worry you can put straight back down, because it stopped me for a few minutes before I did the fix. Dragging a host’s clock forward by two years does not break the host’s own certificate. XAPI issues it with a ten year validity, so a machine installed while it believed it was 2024 is holding one valid until 2034, and correcting the date lands comfortably inside that window. You do not need &lt;code&gt;xe host-refresh-server-certificate&lt;/code&gt; for this. There is a real pool join certificate failure that looks alarming in a vaguely similar way, but it is a different animal and this is not it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Somebody else got there first, in an hour
&lt;/h1&gt;

&lt;p&gt;The best evidence for all of this is not mine, and I want to be clear about that. There is &lt;a href="https://xcp-ng.org/forum/topic/11558" rel="noopener noreferrer"&gt;a forum thread from last November&lt;/a&gt; where the person who hit this worked the whole thing out themselves in under an hour, from the same host inside the same session, and left behind the three observations that make it airtight: the 301 with &lt;code&gt;http://&lt;/code&gt; in the repo file, the honest &lt;code&gt;certificate is not yet valid&lt;/code&gt; with &lt;code&gt;https://&lt;/code&gt;, and then setting NTP and reverting to the original URL and watching it just work. I spent longer than they did and arrived at the same place. There is also &lt;a href="https://xcp-ng.org/forum/topic/9560" rel="noopener noreferrer"&gt;topic 9560&lt;/a&gt;, from 2024, which is the same symptom on 8.2 and simply ends, with nobody ever having named the cause.&lt;/p&gt;

&lt;p&gt;Somebody suggested on that November thread that this deserved a page in the troubleshooting docs. They were right, and it did not happen at the time, which is more or less what the second thread looks like from the outside. There is a documentation change in review now that adds it under the common problems, next to the existing clock entry, plus a line in the update prerequisites saying the plain thing out loud: getting to the repositories needs a correct clock, and the error you get when you do not have one does not mention clocks. It is a small change and I have no idea yet whether it lands in the shape it is currently in, but it should at least mean that three minutes with a search engine gets you the answer rather than two forum threads and a shrug.&lt;/p&gt;

&lt;p&gt;The one line takeaway, because it is the line I wish I had read first: if yum blames a redirect, run &lt;code&gt;date&lt;/code&gt; before you touch the repo file, and if the clock is wrong run &lt;code&gt;chronyc sources&lt;/code&gt;, because a time daemon can be running, enabled, and pointed at absolutely nothing.&lt;/p&gt;

</description>
      <category>chrony</category>
      <category>debugging</category>
      <category>homelab</category>
      <category>ntp</category>
    </item>
    <item>
      <title>The fastest Windows on XCP-ng is the one you don't install</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Tue, 07 Jul 2026 20:19:28 +0000</pubDate>
      <link>https://dev.to/gounthar/the-fastest-windows-on-xcp-ng-is-the-one-you-dont-install-o1m</link>
      <guid>https://dev.to/gounthar/the-fastest-windows-on-xcp-ng-is-the-one-you-dont-install-o1m</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on June 29, 2026 at &lt;a href="https://bruno.verachten.fr/2026/06/29/the-fastest-windows-on-xcp-ng-is-the-one-you-dont-install/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2026/06/29/the-fastest-windows-on-xcp-ng-is-the-one-you-dont-install/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F00-header-fork-in-the-road.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F00-header-fork-in-the-road.jpg" alt="Two forest paths diverging" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by Jens Lelie on &lt;a href="https://unsplash.com/photos/u0vgcIOQG08" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I needed a Windows box on XCP-ng for a side project, and I did the obvious thing first. I tried to install it.&lt;/p&gt;

&lt;p&gt;That was a mistake, and it took me the better part of two days to admit it.&lt;/p&gt;

&lt;p&gt;If you have ever fed a Windows ISO to an XCP-ng VM, you know the wall. Windows Setup boots, you click through the language screen, and then it stops dead on "A media driver your computer needs is missing." XCP-ng hands the VM its disk over the Xen &lt;a href="https://en.wikipedia.org/wiki/Paravirtualization" rel="noopener noreferrer"&gt;paravirtual&lt;/a&gt; block bus, which shows up inside the guest as &lt;code&gt;xvda&lt;/code&gt;, and the &lt;a href="https://en.wikipedia.org/wiki/Windows_Preinstallation_Environment" rel="noopener noreferrer"&gt;WinPE&lt;/a&gt; environment that drives Setup has never heard of it. It is the same situation as installing Windows on &lt;a href="https://en.wikipedia.org/wiki/Proxmox_Virtual_Environment" rel="noopener noreferrer"&gt;Proxmox&lt;/a&gt; or plain &lt;a href="https://en.wikipedia.org/wiki/Kernel-based_Virtual_Machine" rel="noopener noreferrer"&gt;KVM&lt;/a&gt; without the &lt;a href="https://wiki.libvirt.org/Virtio.html" rel="noopener noreferrer"&gt;VirtIO&lt;/a&gt; drivers loaded. The disk is right there, the installer just cannot see it, so the "where do you want to install Windows" screen is empty and "Next" stays greyed out.&lt;/p&gt;

&lt;p&gt;On Proxmox you click "Load driver", point at the VirtIO floppy, and move on. On XCP-ng the equivalent storage driver (&lt;code&gt;xenvbd&lt;/code&gt;, plus its bus parent &lt;code&gt;xenbus&lt;/code&gt;) only ships inside the guest tools MSI, so getting it in front of WinPE is more work. Here is the short version of what I went through, because it is the reason the rest of this post exists.&lt;/p&gt;

&lt;p&gt;I extracted the PV drivers out of &lt;code&gt;XenTools-x64.msi&lt;/code&gt; with &lt;code&gt;msitools&lt;/code&gt; on a Linux box, dropped them into a &lt;code&gt;$WinPEDriver$&lt;/code&gt; folder so WinPE would auto-load them at boot, and built a remastered install ISO. Then I learned that Windows ISOs store their payload in &lt;a href="https://en.wikipedia.org/wiki/Universal_Disk_Format" rel="noopener noreferrer"&gt;UDF&lt;/a&gt;, so the usual &lt;code&gt;xorriso&lt;/code&gt; "add a file" trick copies almost nothing forward. So I did a full extract with &lt;code&gt;7z&lt;/code&gt;, injected the drivers, and rebuilt a dual BIOS and UEFI bootable ISO. Then I learned that WinPE’s &lt;a href="https://en.wikipedia.org/wiki/ISO_9660" rel="noopener noreferrer"&gt;ISO9660&lt;/a&gt; driver cannot read a file larger than 4 GB, and the &lt;code&gt;install.wim&lt;/code&gt; is 5.8 GB, which is exactly why Microsoft uses UDF in the first place. So I split the &lt;a href="https://en.wikipedia.org/wiki/Windows_Imaging_Format" rel="noopener noreferrer"&gt;WIM&lt;/a&gt; with &lt;code&gt;wimsplit&lt;/code&gt; into chunks under 4 GB. And because the VM is headless, I had to catch the "Press any key to boot from CD" prompt by spamming keystrokes at the firmware over &lt;a href="https://wiki.qemu.org/Documentation/QMP" rel="noopener noreferrer"&gt;QMP&lt;/a&gt;, since there is no keyboard at the firmware screen to press.&lt;/p&gt;

&lt;p&gt;I got it working. The disk finally appeared, Setup ran, and then the fresh install bricked into Automatic Repair on first boot. At that point I stopped and asked myself the question I should have asked at the start.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F01-win11-automatic-repair.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F01-win11-automatic-repair.png" alt="Windows Automatic Repair recovery screen reading Your PC did not start correctly, with Restart and Advanced options buttons." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. How the from-scratch install ended: a fresh Windows 11 stuck in Automatic Repair on first boot.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Why am I installing Windows at all?&lt;/p&gt;

&lt;h1&gt;
  
  
  Microsoft hands you a finished disk
&lt;/h1&gt;

&lt;p&gt;The &lt;a href="https://www.microsoft.com/en-us/evalcenter/" rel="noopener noreferrer"&gt;Microsoft Evaluation Center&lt;/a&gt; publishes Windows Server as a ready-made &lt;a href="https://en.wikipedia.org/wiki/VHD_(file_format)" rel="noopener noreferrer"&gt;VHD&lt;/a&gt;, sitting right there next to the ISO. It is a &lt;a href="https://en.wikipedia.org/wiki/Sysprep" rel="noopener noreferrer"&gt;sysprepped&lt;/a&gt;, fully installed copy of Windows on a virtual disk. No Setup, no driver injection, no keystroke roulette at a firmware prompt. You attach the disk to a VM, power it on, answer a couple of out-of-box questions, set a password, and you are looking at a desktop.&lt;/p&gt;

&lt;p&gt;Two caveats before you reach for it, because they matter and I would rather you hear them now than after you have built something on top.&lt;/p&gt;

&lt;p&gt;It is Windows &lt;strong&gt;Server&lt;/strong&gt;, not a client edition like Windows 11. For a lab, a proof of concept, or anything where you just need a Windows userland to run something, that is fine, and Server even comes with the Desktop Experience so it looks and feels like a normal Windows. If you specifically need client Windows, this is not your image.&lt;/p&gt;

&lt;p&gt;It is an &lt;strong&gt;evaluation&lt;/strong&gt; image. The licence runs 180 days, and you are expected to activate it online within the first 10 or it starts shutting itself down on a timer. For a throwaway lab box, that is a feature: it cleans up after itself. For anything you mean to keep, plan to activate it or treat it as disposable.&lt;/p&gt;

&lt;p&gt;Mine was the Datacenter evaluation, build 26100. Here is the whole recipe.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 1: grab the VHD, not the ISO
&lt;/h1&gt;

&lt;p&gt;From the Evaluation Center, pick &lt;a href="https://en.wikipedia.org/wiki/Windows_Server_2025" rel="noopener noreferrer"&gt;Windows Server 2025&lt;/a&gt; and look for the &lt;strong&gt;VHD&lt;/strong&gt; download rather than the ISO. Fill in the short form, and you get a roughly 11 GB &lt;code&gt;.vhdx&lt;/code&gt; file. That file is the entire point: it is Windows already installed, captured as a disk image, waiting to be dropped onto a hypervisor.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 2: convert it, but not on dom0
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;xe vdi-import&lt;/code&gt; understands raw and VHD, but not the newer VHDX container, so the file needs converting first. &lt;code&gt;qemu-img&lt;/code&gt; does that in one line. My instinct was to run it on the XCP-ng host itself, since that is where the disk is going anyway.&lt;/p&gt;

&lt;p&gt;Do not do that. The way it fails is nasty, because it fails quietly. The &lt;code&gt;qemu-img&lt;/code&gt; that ships in &lt;a href="https://wiki.xenproject.org/wiki/Dom0" rel="noopener noreferrer"&gt;dom0&lt;/a&gt; is a stripped-down build that only knows &lt;code&gt;raw&lt;/code&gt; and &lt;code&gt;qcow2&lt;/code&gt;. Point it at a VHDX and, in my case, it did not error out and tell me it could not read the format. It mis-detected the file as &lt;code&gt;raw&lt;/code&gt;, reported the virtual size as equal to the file size (which is nonsense for a dynamic VHDX), and any conversion on top of that produces garbage. You will not find out until much later, when the VM refuses to boot, and you will not know why.&lt;/p&gt;

&lt;p&gt;So convert on a normal machine that has a full &lt;code&gt;qemu-img&lt;/code&gt;. Any modern Linux distribution has one; my &lt;a href="https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux" rel="noopener noreferrer"&gt;WSL2&lt;/a&gt; box was fine. A current &lt;code&gt;qemu-img&lt;/code&gt; reads the VHDX correctly and reports the real geometry: a 64 GiB virtual disk with about 11 GB of actual data inside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qemu-img convert &lt;span class="nt"&gt;-f&lt;/span&gt; vhdx &lt;span class="nt"&gt;-O&lt;/span&gt; vpc &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;subformat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dynamic ws2025.vhdx ws2025.vhd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-O vpc&lt;/code&gt; is the VHD output format (the name is a leftover from &lt;a href="https://en.wikipedia.org/wiki/Windows_Virtual_PC" rel="noopener noreferrer"&gt;Virtual PC&lt;/a&gt;), and &lt;code&gt;subformat=dynamic&lt;/code&gt; keeps it thin, so you end up with a compact dynamic VHD that is about 11 GB on disk despite presenting a 64 GiB drive to the VM.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 3: import the disk
&lt;/h1&gt;

&lt;p&gt;Copy the converted VHD over to the host. It does not need to live on dom0’s root partition, which is usually tiny: put it on the storage repository mount, which has the room. Then create a VDI sized to match the disk’s virtual size exactly, and import the file into it.&lt;/p&gt;

&lt;p&gt;That virtual size is not the round 64 GiB you would expect, and this bit me. When &lt;code&gt;qemu-img&lt;/code&gt; writes a dynamic VHD it rounds the virtual size up to the nearest CHS (cylinder, head, sector) geometry boundary, so a nominal 64 GiB disk comes out as &lt;code&gt;68720517120&lt;/code&gt; bytes, not the arithmetic &lt;code&gt;68719476736&lt;/code&gt;. The VDI has to match the VHD exactly, or be larger, or &lt;code&gt;vdi-import&lt;/code&gt; rejects it for being too small. So give &lt;code&gt;vdi-create&lt;/code&gt; the rounded number that &lt;code&gt;qemu-img&lt;/code&gt; actually produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xe vdi-create sr-uuid&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;SR&amp;gt; name-label&lt;span class="o"&gt;=&lt;/span&gt;ws2025 virtual-size&lt;span class="o"&gt;=&lt;/span&gt;68720517120 &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user
xe vdi-import &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;VDI&amp;gt; &lt;span class="nv"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/run/sr-mount/&amp;lt;SR&amp;gt;/ws2025.vhd &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vhd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The import returns 0 and looks, frankly, like it did nothing. Here is the trap: do not trust the &lt;code&gt;physical-utilisation&lt;/code&gt; field to confirm the data landed. On a thin (ext) storage repository it lied to me, reading about 135 KiB while 11 GB of Windows sat on the disk. It is the same unreliable metric that had me staring at "0 MiB" mid-install during the from-scratch saga.&lt;/p&gt;

&lt;p&gt;The ground truth is the backing file on disk. Run an &lt;code&gt;sr-scan&lt;/code&gt;, then look at the actual &lt;code&gt;.vhd&lt;/code&gt; the SR created for your VDI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /run/sr-mount/&amp;lt;SR&amp;gt;/&amp;lt;VDI&amp;gt;.vhd   &lt;span class="c"&gt;# should report about 11 G&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that reads roughly 11 G, the import worked, whatever the xe metric tells you.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 4: make the VM UEFI, not BIOS
&lt;/h1&gt;

&lt;p&gt;This is the gotcha that cost me the most time, and it cost me time because the internet lied to me with great confidence. Page after page describes the Windows Server evaluation "VHD" as a Generation 1 image, which in &lt;a href="https://en.wikipedia.org/wiki/Hyper-V" rel="noopener noreferrer"&gt;Hyper-V&lt;/a&gt; terms means BIOS firmware and an &lt;a href="https://en.wikipedia.org/wiki/Master_boot_record" rel="noopener noreferrer"&gt;MBR&lt;/a&gt; partition table. So I dutifully built a BIOS VM, attached the disk, set it to boot, and started it.&lt;/p&gt;

&lt;p&gt;The firmware found the disk. It listed it as "XS Virtual IDE Controller, Hard Drive 64 GB", printed "Boot device: Hard Disk, success", and then sat there. Forever. No error, no spinner, just a cursor and silence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F02-bios-hang-boot-device-success.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F02-bios-hang-boot-device-success.png" alt="SeaBIOS power-on text showing an XS Virtual IDE Controller with a 64 GB hard drive detected and the line Boot device: Hard Disk success, with no further output." width="720" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. The BIOS VM finds the disk and reports "Boot device: Hard Disk, success", then hangs. There is no MBR boot code on a GPT disk for it to run.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The image is not Generation 1. For build 26100 it is &lt;a href="https://en.wikipedia.org/wiki/GUID_Partition_Table" rel="noopener noreferrer"&gt;GPT&lt;/a&gt; and UEFI, and you can prove it to yourself in about thirty seconds by looking at the first sectors of the converted VHD. You will find a protective MBR (a single partition entry of type &lt;code&gt;0xEE&lt;/code&gt;, which is GPT’s way of telling old tools "this whole disk is taken, hands off"), and the literal string &lt;code&gt;EFI PART&lt;/code&gt; sitting at LBA 1, which is the GPT header signature. A BIOS VM finds the disk, looks at the MBR for boot code to run, finds only the protective stub, and has nothing to execute. Hence the polite hang.&lt;/p&gt;

&lt;p&gt;The fix is two lines. Build the VM from the "Other install media" template (so XCP-ng does not impose an OS-specific default), then flip it to UEFI and turn off &lt;a href="https://en.wikipedia.org/wiki/UEFI#Secure_Boot" rel="noopener noreferrer"&gt;Secure Boot&lt;/a&gt;. Secure Boot needs Microsoft’s UEFI certificates enrolled in the pool (that is what &lt;code&gt;secureboot-certs install&lt;/code&gt; sets up), and a fresh XCP-ng 8.3 pool does not ship them by default, so a Secure Boot guest has nothing to validate its signed bootloader against until you enroll them. Turning it off is the no-friction path to a booted desktop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xe vm-param-set &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;VM&amp;gt; HVM-boot-params:firmware&lt;span class="o"&gt;=&lt;/span&gt;uefi
xe vm-param-set &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;VM&amp;gt; platform:secureboot&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you would rather keep Secure Boot on, you can, as long as the pool has the certificates. You enroll them once per pool with a single command, and from then on every UEFI guest can use Secure Boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;secureboot-certs &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fetches the standard Microsoft PK, KEK, db and dbx certificates and stores them at the pool level. Check what a pool already holds with &lt;code&gt;secureboot-certs report&lt;/code&gt;; on the pool I tested, all four were already in place, so there was nothing to install. Then build the VM exactly as above, but flip the switch the other way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xe vm-param-set &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;VM&amp;gt; HVM-boot-params:firmware&lt;span class="o"&gt;=&lt;/span&gt;uefi
xe vm-param-set &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;VM&amp;gt; platform:secureboot&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The evaluation disk boots with Secure Boot enforcing, because its bootloader is signed by Microsoft and the pool now holds the certificate that vouches for it. To prove it landed rather than take my word for it, open a shell in the guest and ask Windows directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Confirm-SecureBootUEFI&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;True&lt;/code&gt; means Secure Boot is on and enforcing. On my Windows Server 2025 build 26100 it came back &lt;code&gt;True&lt;/code&gt; on the first boot, with the UEFI &lt;code&gt;SetupMode&lt;/code&gt; variable reading 0, which is the firmware’s way of saying the keys are enrolled and it is checking signatures for real.&lt;/p&gt;

&lt;p&gt;One version caveat, because this leans on the host package behaving. Confirm your dom0 is not running the withdrawn &lt;code&gt;varstored 1.2.0-3.1&lt;/code&gt;, which mishandled certificate enrollment; check with &lt;code&gt;rpm -q varstored&lt;/code&gt;. The pool I tested was on &lt;code&gt;varstored 1.3.2-2.1&lt;/code&gt; (XCP-ng 8.3), where it works cleanly.&lt;/p&gt;

&lt;p&gt;Attach the imported VDI as the bootable disk, give the VM a network interface, set the boot order to disk, and start it.&lt;/p&gt;

&lt;p&gt;First boot runs the usual sysprep "Getting ready" dance and reboots itself once partway through, while Windows specializes the image for this particular VM. Be patient here. The console is painfully slow while that happens, and that is a consequence of how we built this VM rather than something XCP-ng does to every guest. A VM created from the "Other install media" template has no &lt;code&gt;vga&lt;/code&gt; key in its platform map, and with that key unset qemu hands the &lt;a href="https://en.wikipedia.org/wiki/Hardware-assisted_virtualization" rel="noopener noreferrer"&gt;HVM&lt;/a&gt; guest an emulated Cirrus adapter with 4 MB of video memory. "Getting ready" can take close to ten minutes on it, and it looks identical to a hang. The Windows templates set &lt;code&gt;vga: std&lt;/code&gt; and &lt;code&gt;videoram: 8&lt;/code&gt; instead. Hold that thought, because the same template choice is about to cost us something bigger. If you want reassurance that it is actually working and not stuck, check &lt;code&gt;xentop&lt;/code&gt; on the host: a VM that is busy specializing is burning CPU, a hung one is not. After the reboot you land at the out-of-box screens. The US region and keyboard defaults are fine to accept, then you set the built-in Administrator password, and you are at a Windows desktop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F03-getting-ready-uefi-boot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F03-getting-ready-uefi-boot.png" alt="Windows boot screen with the four-square logo and a spinner above the text Getting ready." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. With firmware set to UEFI, the imported disk boots: Windows Server 2025 specializing on first run.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F04-oobe-hi-there.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F04-oobe-hi-there.png" alt="Windows out-of-box Hi there screen with United States region, English language, and US keyboard dropdowns." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4. Out-of-box setup, region and keyboard. No installer ran to get here, only an imported disk.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One pleasant surprise: the network just works, before you have installed a single paravirtual driver. Windows Server 2025 has an inbox driver for the emulated NIC that XCP-ng presents, so the VM picked up a DHCP lease (and even a public IPv6 address) the moment it reached the desktop. RDP and SSH were reachable immediately. I enabled the in-box OpenSSH server so I could drive the machine from a real shell instead of squinting at a slow VNC console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Add-WindowsCapability&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Online&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OpenSSH.Server~~~~0.0.1.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Start-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sshd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Set-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sshd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-StartupType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Automatic&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the from-scratch ordeal, where every keystroke went through a flaky console bridge, having a plain SSH prompt into Windows felt like a small miracle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F05-ipconfig-dhcp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F05-ipconfig-dhcp.png" alt="A Command Prompt running ipconfig, showing the Ethernet adapter with an IPv4 address and a default gateway." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5. The emulated network card has an inbox driver, so the VM pulls a DHCP lease on first boot, before any paravirtual drivers are installed.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The flags that template did not set
&lt;/h1&gt;

&lt;p&gt;Added after publication, because a colleague’s support question sent me back to look at my own VM.&lt;/p&gt;

&lt;p&gt;Building from "Other install media" is the right call for getting this disk to boot, and it has a cost I did not notice at the time. That template is deliberately generic, so it does not apply the &lt;a href="https://docs.xcp-ng.org/vms/#enabling-viridian-extensions" rel="noopener noreferrer"&gt;Viridian settings&lt;/a&gt; that the Windows templates do. Viridian is the platform’s name for the Hyper-V enlightenments Windows expects to find, and the documentation is blunt about it: all six parameters should be true for proper operation of Windows.&lt;/p&gt;

&lt;p&gt;Put the two templates side by side and the gap is obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xe template-param-get &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;Windows Server 2025 template&amp;gt; param-name&lt;span class="o"&gt;=&lt;/span&gt;platform
xe template-param-get &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;Other &lt;span class="nb"&gt;install &lt;/span&gt;media template&amp;gt; param-name&lt;span class="o"&gt;=&lt;/span&gt;platform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Windows Server 2025 template sets all six. "Other install media" sets &lt;code&gt;viridian: true&lt;/code&gt; and stops there. My imported VM inherited the short version, so it had been running on one flag out of six the whole time.&lt;/p&gt;

&lt;p&gt;It is not only Viridian. Put the two platform maps side by side and the Windows template also sets &lt;code&gt;vga: std&lt;/code&gt; and &lt;code&gt;videoram: 8&lt;/code&gt;, which is the slow console from earlier in this post. One template choice, three defaults you do not get.&lt;/p&gt;

&lt;p&gt;Check yours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xe vm-param-get &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;VM&amp;gt; param-name&lt;span class="o"&gt;=&lt;/span&gt;platform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any of &lt;code&gt;viridian&lt;/code&gt;, &lt;code&gt;viridian_time_ref_count&lt;/code&gt;, &lt;code&gt;viridian_reference_tsc&lt;/code&gt;, &lt;code&gt;viridian_apic_assist&lt;/code&gt;, &lt;code&gt;viridian_crash_ctl&lt;/code&gt; or &lt;code&gt;viridian_stimer&lt;/code&gt; is missing, or present and set to &lt;code&gt;false&lt;/code&gt;, set them with the VM shut down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xe vm-param-set &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;VM&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  platform:viridian&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  platform:viridian_time_ref_count&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  platform:viridian_reference_tsc&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  platform:viridian_apic_assist&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  platform:viridian_crash_ctl&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  platform:viridian_stimer&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did measure it in the end, because a claim like this is worth more than a shrug. Same VM, same host, the other four VMs on that host shut down. &lt;a href="https://github.com/microsoft/diskspd" rel="noopener noreferrer"&gt;diskspd&lt;/a&gt; 2.2, five iterations of each of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;diskspd.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c2G&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-W10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-b4K&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-t1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Sh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-w0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\bench\test.dat&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;diskspd.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c2G&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-W5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-b4K&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-t4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Sh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-L&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-w0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\bench\test.dat&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-Sh&lt;/code&gt; turns off Windows-side caching for that file. It does nothing about the layers below the guest, and neither did I, which matters further down. I measured at one flag, flipped to six, went back to one, then back to six, so that a reboot could not quietly take the credit. The first iteration of every run is discarded, because it reads off a warm cache and flatters whichever configuration happens to go first. That is not a theoretical worry: on the queue-depth-1 test the discarded iteration came back at 28.8 MiB/s, against 10.4 to 13.4 for the rest of the run.&lt;/p&gt;

&lt;p&gt;4K random read, medians of eight measurements per group:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;4K random read&lt;/th&gt;
&lt;th&gt;1 of 6 flags&lt;/th&gt;
&lt;th&gt;6 of 6 flags&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queue depth 1, average latency&lt;/td&gt;
&lt;td&gt;0.312 ms&lt;/td&gt;
&lt;td&gt;0.195 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue depth 1, IOPS&lt;/td&gt;
&lt;td&gt;3,143&lt;/td&gt;
&lt;td&gt;5,127&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue depth 8 across 4 threads, IOPS&lt;/td&gt;
&lt;td&gt;45,147&lt;/td&gt;
&lt;td&gt;71,674&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two groups did not overlap, and the run-to-run spread went from roughly a quarter to a few percent.&lt;/p&gt;

&lt;p&gt;Then the honest part, because I would rather nobody changed a production VM on the strength of my afternoon. I flipped five flags at once, so I cannot tell you which one matters, or whether it is one of them or all five together. I cannot explain the mechanism either. I have no good story for why timer and APIC enlightenments would move 4K random read throughput this far, and I can invent one about reducing VM exits, but inventing one is precisely what I am trying to avoid here. It is also possible I measured something that merely correlates with the config change rather than the flags themselves.&lt;/p&gt;

&lt;p&gt;Add to that one VM, one host, and local disks rather than iSCSI or NFS. One of the four runs drifted upward partway through in a way that looks like host caching, which is exactly the layer I never controlled, and which is why the table gives medians and no percentages at all. Please do not quote these as a general figure, and if you know why the difference is this large, I would rather hear it than keep guessing.&lt;/p&gt;

&lt;p&gt;What I am confident about is the first half of this section, because it is only reading template parameters back. A VM built this way gets one flag instead of six, and putting the other five back is cheap.&lt;/p&gt;

&lt;p&gt;The documentation attributes a harder failure to the same missing synthetic timer: a Windows Server 2025 guest &lt;a href="https://docs.xcp-ng.org/troubleshooting/windows-pv-tools/#windows-server-2025-hangs-randomly-with-0-cpu" rel="noopener noreferrer"&gt;hangs randomly at 0% CPU&lt;/a&gt;. The flags are the first thing that page tells you to check, and they only cover the host side of it. If you have ever set Windows boot parameters to disable the synthetic timer, those have to come back out too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;bcdedit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/deletevalue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{current}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;useplatformclock&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;bcdedit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/deletevalue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{current}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;useplatformtick&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That wants an elevated prompt, and it wants a reboot: the BCD store is read at boot and nowhere else, so nothing changes until the guest comes back up.&lt;/p&gt;

&lt;p&gt;The same gap catches anything that did not come from a Windows template, such as a VM imported from a VHD like this one. Migrating in from VMware is less clear-cut than I first assumed: the &lt;a href="https://docs.xcp-ng.org/vms/#enabling-viridian-extensions" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; says Viridian "is also enabled on V2V-migrated and Packer-created VMs if using the correct templates", so the outcome depends on what the migration landed on. I have not run a V2V import myself, so read the platform map rather than assuming it either way.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 5: the guest tools, and the one real trap
&lt;/h1&gt;

&lt;p&gt;The emulated NIC works, but you still want the XCP-ng guest tools. They bring the paravirtual network and storage drivers (&lt;code&gt;xennet&lt;/code&gt; and &lt;code&gt;xenvbd&lt;/code&gt;), which are faster than the emulated devices, and the management agent that reports the guest’s IP back to the host so &lt;a href="https://xapi-project.github.io/" rel="noopener noreferrer"&gt;XAPI&lt;/a&gt; and Xen Orchestra can show you where the VM actually is on the network. Installing them is where the one true footgun of this whole exercise lives, so slow down for this part.&lt;/p&gt;

&lt;p&gt;There is a VM setting called &lt;code&gt;has-vendor-device&lt;/code&gt;. The name sounds like exactly what you would want on: something for the PV drivers to bind to. That intuition is precisely wrong. I turned it on, rebooted with the network up, and Windows Update quietly reached out and installed &lt;strong&gt;Citrix’s&lt;/strong&gt; PV drivers underneath me, without asking. Because &lt;code&gt;has-vendor-device=true&lt;/code&gt; is not "let the PV drivers bind". It &lt;em&gt;is&lt;/em&gt; the "Manage Citrix PV drivers via Windows Update" feature. Flip it on, give Windows a network, and Update treats that as permission to manage your Xen drivers for you.&lt;/p&gt;

&lt;p&gt;In my case they installed broken. The bus driver came up, but its children sat in an error state, so the NIC never moved off the emulated device anyway. Worse, the XCP-ng &lt;code&gt;XenTools-x64.msi&lt;/code&gt; then refused to install at all. It checks for exactly this situation, found the Citrix drivers already present, threw "incompatible devices found", and exited 1603. So the helpful Windows Update behaviour had locked me out of the supported tools.&lt;/p&gt;

&lt;p&gt;The supported path, straight from the XCP-ng docs, is the opposite of what the setting’s name suggests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Leave &lt;code&gt;has-vendor-device&lt;/code&gt; set to &lt;strong&gt;false&lt;/strong&gt;. That is the expected, documented state for XCP-ng’s own tools. The PV drivers bind to the always-present base Xen platform device regardless; the vendor device is &lt;em&gt;only&lt;/em&gt; the Windows Update trigger, and you do not want that trigger pulled.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If Windows Update already grabbed the Citrix drivers (as mine had), run &lt;code&gt;XenClean.exe&lt;/code&gt; from the guest tools CD as administrator. It rips out the Citrix drivers and reboots itself, no Safe Mode gymnastics required.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Then run &lt;code&gt;XenTools-x64.msi&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After XenClean, the tools install, and one reboot later every &lt;code&gt;xen*&lt;/code&gt; device came up clean in Device Manager, the NIC switched over to the "XCP-ng PV Network Device", and the host started reporting the guest’s IP. The PV driver version was 9.1.200.0, the current release when I tested. Done.&lt;/p&gt;

&lt;p&gt;One component in that package is worth calling out separately if your VM is UEFI, which this one is: the XCP-ng Standard VGA Display Driver. The &lt;a href="https://docs.xcp-ng.org/vms/#uefi-vm" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; says a UEFI VM needs it before Windows screen resolution can be changed from inside the OS at all, which is a fair description of the console I have grumbled about twice in this post. Whether it also shortens the "Getting ready" wait I have not measured, so treat that as the docs' claim about resolution and nothing more.&lt;/p&gt;

&lt;p&gt;The one-line takeaway, because it is the line I wish I had read first: leave &lt;code&gt;has-vendor-device&lt;/code&gt; off, and let XCP-ng’s MSI install the drivers. Turning it on hands your driver management to Windows Update and Citrix, and breaks the supported path.&lt;/p&gt;

&lt;h1&gt;
  
  
  The payoff
&lt;/h1&gt;

&lt;p&gt;From a downloaded VHD to a running, networked, paravirtualised Windows Server, with the disk saved for reuse, took me well under an hour the second time around. Compare that with the day and a half I burned teaching the installer to find its own disk, only to watch the result brick on first boot. When the platform already hands you a finished disk image, importing it beats installing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F06-server-manager-desktop.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2F2026%2F06%2F29%2F06-server-manager-desktop.png" alt="Windows Server 2025 desktop showing the Server Manager dashboard with the local server and roles listed." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 6. The payoff: a full Windows Server desktop on XCP-ng, and the installer never ran once.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I keep the converted VHD in a small image library now, with a plain text file next to it listing these exact steps. Next time I need a throwaway Windows on XCP-ng, it is a copy and an import away, and the hard part is already done.&lt;/p&gt;

&lt;p&gt;As for why I wanted a Windows VM on XCP-ng in the first place, that involves an Intel iGPU, &lt;a href="https://en.wikipedia.org/wiki/VGA_passthrough" rel="noopener noreferrer"&gt;GPU passthrough&lt;/a&gt;, and a hardware video encoder that only exists on the Windows side of the fence. But that is a story for the next post.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>hypervisor</category>
      <category>qemu</category>
      <category>vhd</category>
    </item>
    <item>
      <title>Twelve Years, One Bet on PowerPC: The Power Progress Community Story</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Tue, 12 May 2026 20:44:24 +0000</pubDate>
      <link>https://dev.to/gounthar/twelve-years-one-bet-on-powerpc-the-power-progress-community-story-1lip</link>
      <guid>https://dev.to/gounthar/twelve-years-one-bet-on-powerpc-the-power-progress-community-story-1lip</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on May 12, 2026 at &lt;a href="https://bruno.verachten.fr/2026/05/12/twelve-years-one-bet-on-powerpc-the-power-progress-community-story/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2026/05/12/twelve-years-one-bet-on-powerpc-the-power-progress-community-story/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In 2014, a small group of Italian volunteers decided to build an open hardware &lt;a href="https://en.wikipedia.org/wiki/PowerPC" rel="noopener noreferrer"&gt;PowerPC&lt;/a&gt; laptop from scratch. No salary. No investor deck. No deadline. Just a shared conviction that the world needed an open computer built on an open architecture, running free software, designed by and for the community.&lt;/p&gt;

&lt;p&gt;Twelve years later, they’re still at it. I recently spoke with &lt;a href="https://www.sfscon.it/speakers/roberto-innocenti/" rel="noopener noreferrer"&gt;Roberto Innocenti&lt;/a&gt;, president of the &lt;a href="https://powerprogress.org/" rel="noopener noreferrer"&gt;Power Progress Community&lt;/a&gt;, and the conversation stayed with me for days.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://www.pexels.com/@113472439/" rel="noopener noreferrer"&gt;Сергей&lt;/a&gt; on &lt;a href="https://www.pexels.com/photo/view-of-a-winding-road-9695725/" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Who they are
&lt;/h1&gt;

&lt;p&gt;Power Progress Community is a non-profit registered in Italy. Every member is a volunteer. Nobody gets paid except the electronic engineers they hire for specific funded tasks. Roberto describes their "business plan" as deliberately flexible: no crowdfunding countdown clock, no tight time-to-market pressure. Just an open donation campaign they can sustain indefinitely.&lt;/p&gt;

&lt;p&gt;Their mission reads like something from the early free software movement. Solidarity-based knowledge. Freedom of choice. Technology designed to resist surveillance capitalism. Open hardware accessible to people far from the world of computing. They run coding workshops in Italian schools using repurposed old hardware. They partner with universities so students can design open hardware as thesis projects.&lt;/p&gt;

&lt;p&gt;This is not a startup. It’s something older and stranger: a community running on passion.&lt;/p&gt;

&lt;h1&gt;
  
  
  The technology
&lt;/h1&gt;

&lt;p&gt;The project centers on a PowerPC laptop and now a desktop, built around the &lt;a href="https://www.nxp.com/products/processors-and-microcontrollers/power-architecture/qoriq-communication-processors/t-series/qoriq-t2080-multicore-communication-processor:T2080" rel="noopener noreferrer"&gt;NXP T2080&lt;/a&gt;. That’s a 64-bit &lt;a href="https://en.wikipedia.org/wiki/Power_Architecture" rel="noopener noreferrer"&gt;Power Architecture&lt;/a&gt; chip with four dual-threaded e6500 cores and &lt;a href="https://en.wikipedia.org/wiki/AltiVec" rel="noopener noreferrer"&gt;AltiVec&lt;/a&gt; vector support. The desktop version, called the &lt;a href="https://powerpc-notebook.org/" rel="noopener noreferrer"&gt;&lt;strong&gt;Powerboard Tyche&lt;/strong&gt;&lt;/a&gt;, comes in &lt;a href="https://en.wikipedia.org/wiki/MicroATX" rel="noopener noreferrer"&gt;Micro ATX&lt;/a&gt; form factor with two &lt;a href="https://en.wikipedia.org/wiki/PCI_Express" rel="noopener noreferrer"&gt;PCIe&lt;/a&gt; x16 video card slots, two &lt;a href="https://en.wikipedia.org/wiki/NVM_Express" rel="noopener noreferrer"&gt;NVMe&lt;/a&gt; slots, and dual gigabit Ethernet.&lt;/p&gt;

&lt;p&gt;Why PowerPC? Roberto puts it bluntly: "The PowerPC architecture design is newer than the other successful CPU architectures: x86 (1978), MIPS (1981), ARM (1983) — PowerPC (1991)." More to the point, since August 2019 the &lt;a href="https://en.wikipedia.org/wiki/Power_ISA" rel="noopener noreferrer"&gt;Power ISA&lt;/a&gt; became fully open. Developers can now design chips based on the royalty-free specification. The architecture that once ran inside every Mac, every PlayStation 3, and every Nintendo Wii is now genuinely free silicon.&lt;/p&gt;

&lt;p&gt;The Powerboard Tyche schematics are published under the &lt;a href="https://cern-ohl.web.cern.ch/" rel="noopener noreferrer"&gt;CERN Open Hardware Licence&lt;/a&gt; 1.2. PCB source files are on &lt;a href="https://gitlab.com/power-progress-community/" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;. They’re pursuing &lt;a href="https://www.oshwa.org/" rel="noopener noreferrer"&gt;OSHWA&lt;/a&gt; certification. NXP itself agreed to let them publish the T2080 reference design as open hardware. That last part is not a given with any chip manufacturer.&lt;/p&gt;

&lt;h1&gt;
  
  
  Twelve years in the making
&lt;/h1&gt;

&lt;p&gt;The timeline is a story of persistence against everything that can go wrong in open hardware.&lt;/p&gt;

&lt;p&gt;The project started in 2014 with a laptop idea. The formal association launched in 2016. By 2017 they were running their first crowdfunding campaign, partnering with &lt;a href="https://www.acube-systems.biz/" rel="noopener noreferrer"&gt;ACube Systems&lt;/a&gt; for electrical engineering and with &lt;a href="https://slimbook.es/" rel="noopener noreferrer"&gt;Slimbook&lt;/a&gt; (a Spanish Linux laptop maker) for the chassis. By 2022 they had physical prototypes.&lt;/p&gt;

&lt;p&gt;Then came the hard part.&lt;/p&gt;

&lt;p&gt;The prototype booted fine on NXP’s reference development kit. It refused to boot on their custom board. For eighteen months, from November 2023 to April 2025, the team tried every fix they could think of. &lt;a href="https://en.wikipedia.org/wiki/Complex_programmable_logic_device" rel="noopener noreferrer"&gt;CPLD&lt;/a&gt; updates. Firmware comparisons. Desoldering chips to isolate the fault. The board still wouldn’t boot.&lt;/p&gt;

&lt;p&gt;In May 2025, after consulting their donor community, they made a call: pivot to a desktop first. Simpler enclosure. Lower production cost. Merged design based on the updated NXP T2080 reference board. In June 2025, NXP provided the latest T2080 RDB design. In October 2025, schematics were done. By March 2026, the PCB layout was complete.&lt;/p&gt;

&lt;p&gt;That pivot isn’t a failure. It’s twelve years of knowing when to adapt without dropping the goal.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why this matters
&lt;/h1&gt;

&lt;p&gt;At &lt;a href="https://vates.tech/" rel="noopener noreferrer"&gt;Vates&lt;/a&gt;, the company behind the &lt;a href="https://xcp-ng.org/" rel="noopener noreferrer"&gt;XCP-ng&lt;/a&gt; open source hypervisor, there’s an ongoing effort to port &lt;a href="https://xenproject.org/" rel="noopener noreferrer"&gt;Xen&lt;/a&gt; to &lt;a href="https://riscv.org/" rel="noopener noreferrer"&gt;RISC-V&lt;/a&gt;, another open instruction set architecture that barely existed a decade ago. XCP-ng itself isn’t there yet. But the direction is clear: open silicon, community governance, computing that isn’t owned by a handful of companies. Watching Power Progress Community work through a decade of non-x86 hardware development feels like the same conversation.&lt;/p&gt;

&lt;p&gt;There’s also an audience overlap worth naming. The open source sysadmin and virtualization world is full of people who came from Amiga, PowerPC Macs, and older non-x86 systems. People who remember when computing was more architecturally diverse, and think it should be again. If that’s you, or if you simply believe computing is healthier with more than two or three viable architectures, you probably want to know this project exists.&lt;/p&gt;

&lt;h1&gt;
  
  
  Where they are now and how to help
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Update (2026-11-02)&lt;/strong&gt;: the prototype campaign reached its goal: €9,395 donated against a €9,100 target when I checked, enough to pay the manufacturer for three Powerboard Tyche desktop prototypes (not the five mentioned below). The firmware and hardware engineers are now testing them: the CPLD and uPic are programmed and the first SPD messages show on the console. Their post doesn’t report a boot yet. The project also says it has been awarded a grant from the &lt;a href="https://nlnet.nl/commonsfund/" rel="noopener noreferrer"&gt;NGI Zero Commons Fund&lt;/a&gt; through NLnet, with the first funds expected in December. Details in their post, &lt;a href="https://www.powerpc-notebook.org/2026/10/48-hours-to-balance-the-prototypes-the-final-effort/" rel="noopener noreferrer"&gt;48 hours to balance the prototypes: the final effort&lt;/a&gt; (30 October 2026). The paragraph below is the state of things in May.&lt;/p&gt;

&lt;p&gt;The Powerboard Tyche desktop PCB design is done. The &lt;a href="https://www.kicad.org/" rel="noopener noreferrer"&gt;KiCad&lt;/a&gt; schematics are published. What’s left is funding, in two steps. First, roughly €3,500 to balance the outstanding payment on the PCB design itself. Once that’s settled, they can commit to the manufacturer and open the campaign to build the first five physical prototypes, which needs around €9,000 in total. Modest sums relative to what’s already been invested in engineering and community time.&lt;/p&gt;

&lt;p&gt;If you want to follow the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Project updates and timeline: &lt;a href="https://powerpc-notebook.org/" rel="noopener noreferrer"&gt;powerpc-notebook.org&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Donation campaign: &lt;a href="https://fundraising.powerpc-notebook.org/" rel="noopener noreferrer"&gt;fundraising.powerpc-notebook.org&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Community forum: &lt;a href="https://forum.powerprogress.org/" rel="noopener noreferrer"&gt;forum.powerprogress.org&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GitLab (schematics, PCB sources): &lt;a href="https://gitlab.com/power-progress-community/" rel="noopener noreferrer"&gt;gitlab.com/power-progress-community&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Roberto has presented at &lt;a href="https://www.sfscon.it/speakers/roberto-innocenti/" rel="noopener noreferrer"&gt;SFScon&lt;/a&gt; (the South Tyrol Free Software Conference in Bolzano) every year since 2018. His 2025 talk covers the full story from laptop prototype struggles to the desktop pivot. The slides are &lt;a href="https://www.sfscon.it/wp-content/uploads/2025/11/1762214372748sfscon_2025_Open_Hardware_PowerPC_Desktop.pdf" rel="noopener noreferrer"&gt;available here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Open hardware is slow work. Slower than software, slower than cloud services, slower than anything the venture capital cycle rewards. The Power Progress Community has been doing it anyway. Twelve years. Volunteer time. Donor funding. University partnerships. One prototype that wouldn’t boot, and the stubbornness to redesign rather than quit.&lt;/p&gt;

&lt;p&gt;The PCB is done. I hope the prototype isn’t far behind.&lt;/p&gt;

</description>
      <category>openhardware</category>
      <category>powerpc</category>
      <category>community</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Compile FFmpeg on the Odroid XU4 to get hardware acceleration</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Wed, 22 Apr 2026 07:44:26 +0000</pubDate>
      <link>https://dev.to/gounthar/compile-ffmpeg-on-the-odroid-xu4-to-get-hardware-acceleration-14b8</link>
      <guid>https://dev.to/gounthar/compile-ffmpeg-on-the-odroid-xu4-to-get-hardware-acceleration-14b8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on November 28, 2019 at &lt;a href="https://bruno.verachten.fr/2019/11/28/compile-ffmpeg-on-the-odroid-xu4-to-get-hardware-acceleration/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2019/11/28/compile-ffmpeg-on-the-odroid-xu4-to-get-hardware-acceleration/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get build-dep ffmpeg

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;debhelper libchromaprint-dev frei0r-plugins-dev libgnutls28-dev libx265-dev ladspa-sdk libaom-dev libserd-dev libsord-dev libsratom-dev libsndfile1-dev libiec61883-dev libavc1394-dev libavc1394-0 libass-dev libbluray-dev libbs2b-dev libcaca-dev

 &lt;span class="nb"&gt;mkdir &lt;/span&gt;ffmpeg_sources
   36  &lt;span class="nb"&gt;cd &lt;/span&gt;ffmpeg_sources
   37  wget http://deb.debian.org/debian/pool/main/f/ffmpeg/ffmpeg_4.1.4.orig.tar.xz
   38  xz &lt;span class="nt"&gt;-d&lt;/span&gt; ffmpeg_4.1.4.orig.tar.xz
   39  &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xf&lt;/span&gt; ffmpeg_4.1.4.orig.tar
   40  wget http://deb.debian.org/debian/pool/main/f/ffmpeg/ffmpeg_4.1.4.orig.tar.xz
   41  ll
   42  &lt;span class="nb"&gt;cd &lt;/span&gt;ffmpeg-4.1.4/
   43  wget http://deb.debian.org/debian/pool/main/f/ffmpeg/ffmpeg_4.1.4-1~deb10u1.debian.tar.xz
   44  xz &lt;span class="nt"&gt;-d&lt;/span&gt; ffmpeg_4.1.4-1~deb10u1.debian.tar.xz
   45  ll
   46  &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xf&lt;/span&gt; ffmpeg_4.1.4-1~deb10u1.debian.tar
   47  ll
   48  &lt;span class="nb"&gt;rm &lt;/span&gt;ffmpeg_4.1.4-1~deb10u1.debian.tar

&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; dpkg-buildpackage &lt;span class="nt"&gt;-us&lt;/span&gt; &lt;span class="nt"&gt;-uc&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;

ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; bbb_sunflower_1080p_30fps_stereo_abl.mp4 &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-vcodec&lt;/span&gt; h264_v4l2m2m &lt;span class="nt"&gt;-b&lt;/span&gt;:v 2M &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; nv21 output.mp4
ffmpeg version 4.1.4-1~deb10u1 Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; 2000-2019 the FFmpeg developers
  built with gcc 8 &lt;span class="o"&gt;(&lt;/span&gt;Debian 8.3.0-6&lt;span class="o"&gt;)&lt;/span&gt;
  configuration: &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr &lt;span class="nt"&gt;--extra-version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'1~deb10u1'&lt;/span&gt; &lt;span class="nt"&gt;--toolchain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;hardened &lt;span class="nt"&gt;--libdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/lib/arm-linux-gnueabihf &lt;span class="nt"&gt;--incdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/include/arm-linux-gnueabihf &lt;span class="nt"&gt;--arch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arm &lt;span class="nt"&gt;--enable-gpl&lt;/span&gt; &lt;span class="nt"&gt;--disable-stripping&lt;/span&gt; &lt;span class="nt"&gt;--enable-avresample&lt;/span&gt; &lt;span class="nt"&gt;--disable-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;resample &lt;span class="nt"&gt;--enable-avisynth&lt;/span&gt; &lt;span class="nt"&gt;--enable-gnutls&lt;/span&gt; &lt;span class="nt"&gt;--enable-ladspa&lt;/span&gt; &lt;span class="nt"&gt;--enable-libaom&lt;/span&gt; &lt;span class="nt"&gt;--enable-libass&lt;/span&gt; &lt;span class="nt"&gt;--enable-libbluray&lt;/span&gt; &lt;span class="nt"&gt;--enable-libbs2b&lt;/span&gt; &lt;span class="nt"&gt;--enable-libcaca&lt;/span&gt; &lt;span class="nt"&gt;--enable-libcdio&lt;/span&gt; &lt;span class="nt"&gt;--enable-libcodec2&lt;/span&gt; &lt;span class="nt"&gt;--enable-libflite&lt;/span&gt; &lt;span class="nt"&gt;--enable-libfontconfig&lt;/span&gt; &lt;span class="nt"&gt;--enable-libfreetype&lt;/span&gt; &lt;span class="nt"&gt;--enable-libfribidi&lt;/span&gt; &lt;span class="nt"&gt;--enable-libgme&lt;/span&gt; &lt;span class="nt"&gt;--enable-libgsm&lt;/span&gt; &lt;span class="nt"&gt;--enable-libjack&lt;/span&gt; &lt;span class="nt"&gt;--enable-libmp3lame&lt;/span&gt; &lt;span class="nt"&gt;--enable-libmysofa&lt;/span&gt; &lt;span class="nt"&gt;--enable-libopenjpeg&lt;/span&gt; &lt;span class="nt"&gt;--enable-libopenmpt&lt;/span&gt; &lt;span class="nt"&gt;--enable-libopus&lt;/span&gt; &lt;span class="nt"&gt;--enable-libpulse&lt;/span&gt; &lt;span class="nt"&gt;--enable-librsvg&lt;/span&gt; &lt;span class="nt"&gt;--enable-librubberband&lt;/span&gt; &lt;span class="nt"&gt;--enable-libshine&lt;/span&gt; &lt;span class="nt"&gt;--enable-libsnappy&lt;/span&gt; &lt;span class="nt"&gt;--enable-libsoxr&lt;/span&gt; &lt;span class="nt"&gt;--enable-libspeex&lt;/span&gt; &lt;span class="nt"&gt;--enable-libssh&lt;/span&gt; &lt;span class="nt"&gt;--enable-libtheora&lt;/span&gt; &lt;span class="nt"&gt;--enable-libtwolame&lt;/span&gt; &lt;span class="nt"&gt;--enable-libvidstab&lt;/span&gt; &lt;span class="nt"&gt;--enable-libvorbis&lt;/span&gt; &lt;span class="nt"&gt;--enable-libvpx&lt;/span&gt; &lt;span class="nt"&gt;--enable-libwavpack&lt;/span&gt; &lt;span class="nt"&gt;--enable-libwebp&lt;/span&gt; &lt;span class="nt"&gt;--enable-libx265&lt;/span&gt; &lt;span class="nt"&gt;--enable-libxml2&lt;/span&gt; &lt;span class="nt"&gt;--enable-libxvid&lt;/span&gt; &lt;span class="nt"&gt;--enable-libzmq&lt;/span&gt; &lt;span class="nt"&gt;--enable-libzvbi&lt;/span&gt; &lt;span class="nt"&gt;--enable-lv2&lt;/span&gt; &lt;span class="nt"&gt;--enable-omx&lt;/span&gt; &lt;span class="nt"&gt;--enable-openal&lt;/span&gt; &lt;span class="nt"&gt;--enable-opengl&lt;/span&gt; &lt;span class="nt"&gt;--enable-sdl2&lt;/span&gt; &lt;span class="nt"&gt;--enable-libdc1394&lt;/span&gt; &lt;span class="nt"&gt;--enable-libdrm&lt;/span&gt; &lt;span class="nt"&gt;--enable-libiec61883&lt;/span&gt; &lt;span class="nt"&gt;--enable-chromaprint&lt;/span&gt; &lt;span class="nt"&gt;--enable-frei0r&lt;/span&gt; &lt;span class="nt"&gt;--enable-libx264&lt;/span&gt; &lt;span class="nt"&gt;--enable-shared&lt;/span&gt;
  libavutil      56. 22.100 / 56. 22.100
  libavcodec     58. 35.100 / 58. 35.100
  libavformat    58. 20.100 / 58. 20.100
  libavdevice    58.  5.100 / 58.  5.100
  libavfilter     7. 40.101 /  7. 40.101
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  3.100 /  5.  3.100
  libswresample   3.  3.100 /  3.  3.100
  libpostproc    55.  3.100 / 55.  3.100
Input &lt;span class="c"&gt;#0, mov,mp4,m4a,3gp,3g2,mj2, from 'bbb_sunflower_1080p_30fps_stereo_abl.mp4':&lt;/span&gt;
  Metadata:
    major_brand     : isom
    minor_version   : 1
    compatible_brands: isomavc1
    creation_time   : 2013-12-16T17:46:34.000000Z
    title           : Big Buck Bunny, Sunflower version
    artist          : Blender Foundation 2008, Janus Bager Kristensen 2013
    comment         : Creative Commons Attribution 3.0 - http://bbb3d.renderfarming.net
    genre           : Animation
    composer        : Sacha Goedegebure
  Duration: 00:10:34.53, start: 0.000000, bitrate: 5482 kb/s
    Stream &lt;span class="c"&gt;#0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x2160 [SAR 1:1 DAR 8:9], 4999 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default)&lt;/span&gt;
    Metadata:
      creation_time   : 2013-12-16T17:46:34.000000Z
      handler_name    : GPAC ISO Video Handler
    Stream &lt;span class="c"&gt;#0:1(und): Audio: mp3 (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 160 kb/s (default)&lt;/span&gt;
    Metadata:
      creation_time   : 2013-12-16T17:46:41.000000Z
      handler_name    : GPAC ISO Audio Handler
    Stream &lt;span class="c"&gt;#0:2(und): Audio: ac3 (ac-3 / 0x332D6361), 48000 Hz, 5.1(side), fltp, 320 kb/s (default)&lt;/span&gt;
    Metadata:
      creation_time   : 2013-12-16T17:46:41.000000Z
      handler_name    : GPAC ISO Audio Handler
    Side data:
      audio service &lt;span class="nb"&gt;type&lt;/span&gt;: main
Stream mapping:
  Stream &lt;span class="c"&gt;#0:0 -&amp;gt; #0:0 (h264 (native) -&amp;gt; h264 (h264_v4l2m2m))&lt;/span&gt;
Press &lt;span class="o"&gt;[&lt;/span&gt;q] to stop, &lt;span class="o"&gt;[&lt;/span&gt;?] &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;help&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;h264_v4l2m2m @ 0x575a90] driver &lt;span class="s1"&gt;'exynos-gsc'&lt;/span&gt; on card &lt;span class="s1"&gt;'exynos-gsc gscaler'&lt;/span&gt;
    Last message repeated 1 &lt;span class="nb"&gt;times&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;h264_v4l2m2m @ 0x575a90] driver &lt;span class="s1"&gt;'s5p-mfc'&lt;/span&gt; on card &lt;span class="s1"&gt;'s5p-mfc-enc'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;h264_v4l2m2m @ 0x575a90] Using device /dev/video11
&lt;span class="o"&gt;[&lt;/span&gt;h264_v4l2m2m @ 0x575a90] driver &lt;span class="s1"&gt;'s5p-mfc'&lt;/span&gt; on card &lt;span class="s1"&gt;'s5p-mfc-enc'&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;h264_v4l2m2m @ 0x575a90] h264 profile not found
&lt;span class="o"&gt;[&lt;/span&gt;h264_v4l2m2m @ 0x575a90] Encoder adjusted: qmin &lt;span class="o"&gt;(&lt;/span&gt;0&lt;span class="o"&gt;)&lt;/span&gt;, qmax &lt;span class="o"&gt;(&lt;/span&gt;51&lt;span class="o"&gt;)&lt;/span&gt;
Output &lt;span class="c"&gt;#0, mp4, to 'output.mp4':&lt;/span&gt;
  Metadata:
    major_brand     : isom
    minor_version   : 1
    compatible_brands: isomavc1
    composer        : Sacha Goedegebure
    title           : Big Buck Bunny, Sunflower version
    artist          : Blender Foundation 2008, Janus Bager Kristensen 2013
    comment         : Creative Commons Attribution 3.0 - http://bbb3d.renderfarming.net
    genre           : Animation
    encoder         : Lavf58.20.100
    Stream &lt;span class="c"&gt;#0:0(und): Video: h264 (h264_v4l2m2m) (avc1 / 0x31637661), nv21, 1920x2160 [SAR 1:1 DAR 8:9], q=2-31, 2000 kb/s, 30 fps, 15360 tbn, 30 tbc (default)&lt;/span&gt;
    Metadata:
      creation_time   : 2013-12-16T17:46:34.000000Z
      handler_name    : GPAC ISO Video Handler
      encoder         : Lavc58.35.100 h264_v4l2m2m
&lt;span class="o"&gt;[&lt;/span&gt;mp4 @ 0x5695f0] Non-monotonous DTS &lt;span class="k"&gt;in &lt;/span&gt;output stream 0:0&lt;span class="p"&gt;;&lt;/span&gt; previous: 0, current: 0&lt;span class="p"&gt;;&lt;/span&gt; changing to 1. This may result &lt;span class="k"&gt;in &lt;/span&gt;incorrect timestamps &lt;span class="k"&gt;in &lt;/span&gt;the output file.
&lt;span class="o"&gt;[&lt;/span&gt;mp4 @ 0x5695f0] Non-monotonous DTS &lt;span class="k"&gt;in &lt;/span&gt;output stream 0:0&lt;span class="p"&gt;;&lt;/span&gt; previous: 832000, current: 0&lt;span class="p"&gt;;&lt;/span&gt; changing to 832001. This may result &lt;span class="k"&gt;in &lt;/span&gt;incorrect timestamps &lt;span class="k"&gt;in &lt;/span&gt;the output file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>armbian</category>
      <category>ffmpeg</category>
      <category>h264</category>
      <category>odroid</category>
    </item>
    <item>
      <title>Terminal recording on the OrangePi Zero</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Wed, 22 Apr 2026 07:44:23 +0000</pubDate>
      <link>https://dev.to/gounthar/terminal-recording-on-the-orangepi-zero-aad</link>
      <guid>https://dev.to/gounthar/terminal-recording-on-the-orangepi-zero-aad</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on January 3, 2020 at &lt;a href="https://bruno.verachten.fr/2020/01/03/terminal-recording-on-the-orangepi-zero/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2020/01/03/terminal-recording-on-the-orangepi-zero/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Terminal recording on the OrangePi Zero
&lt;/h1&gt;

&lt;p&gt;Lately, I started writing a few articles on the OrangePi &lt;a href="http://www.orangepi.org/orangepizerolts/" rel="noopener noreferrer"&gt;Zero&lt;/a&gt;, as I am trying to use it as part of a conference recording tool and to reference what I know about this board on the OrangePi Community docs &lt;a href="https://www.sbc-community.org/" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Of course, I have to show my command lines, and their results.&lt;/p&gt;

&lt;p&gt;I used to post my commands on &lt;a href="https://gist.github.com/gounthar" rel="noopener noreferrer"&gt;gist&lt;/a&gt;, then use link:&lt;a href="https://carbon.now.sh/?bg=rgba(171%2C" rel="noopener noreferrer"&gt;https://carbon.now.sh/?bg=rgba(171%2C&lt;/a&gt; 184%2C 195%2C 1[Carbon]&amp;amp;t=seti&amp;amp;wt=none&amp;amp;l=auto&amp;amp;ds=true&amp;amp;dsyoff=20px&amp;amp;dsblur=68px&amp;amp;wc=true&amp;amp;wa=true&amp;amp;pv=56px&amp;amp;ph=56px&amp;amp;ln=false&amp;amp;fm=Hack&amp;amp;fs=14px&amp;amp;lh=133%&amp;amp;si=false&amp;amp;es=2x&amp;amp;wm=false) to generate an image with the commands and their outputs… But that’s quite a lot of boring work…&lt;/p&gt;

&lt;p&gt;I then discovered &lt;a href="https://asciinema.org/" rel="noopener noreferrer"&gt;asciinema&lt;/a&gt; and the recording that can be transformed into a GIF file thanks to &lt;a href="https://github.com/asciinema/asciicast2gif?source=post_page---------------------------" rel="noopener noreferrer"&gt;asciicast2gif&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It worked pretty well under Ubuntu for my previous projects, but it was not really practical to launch it, issue an ssh command, and then exit twice before having the recording… and modifying it to get rid of the SSH command.&lt;/p&gt;

&lt;p&gt;Furthermore, GIF are not usable for visually impaired people, so using something like &lt;a href="https://en.wikipedia.org/wiki/Scalable_Vector_Graphics" rel="noopener noreferrer"&gt;SVG &lt;/a&gt;where they could copy and paste would be better in my opinion…&lt;/p&gt;

&lt;p&gt;Unfortunately, SVG is not handled by medium, so it will have to be used on other media, like my Ghost instance (not available yet).&lt;/p&gt;

&lt;p&gt;Enters &lt;a href="https://github.com/nbedos/termtosvg" rel="noopener noreferrer"&gt;termtosvg&lt;/a&gt;. That’s a wonderful tool aimed at producing animated SVG after recording terminal sessions.&lt;/p&gt;

&lt;p&gt;It compiled like a breeze (well, a strong, smelly and cold breeze) on the Zero, so I used it quite intensively (before discovering it could not be used on Medium).&lt;/p&gt;

&lt;p&gt;I then searched for tools able to transform an animated SVG to &lt;a href="https://en.wikipedia.org/wiki/GIF" rel="noopener noreferrer"&gt;GIF&lt;/a&gt;, so that I would not have to redo all my terminal recordings… and was not able to do so.&lt;/p&gt;

&lt;p&gt;There is one library called &lt;a href="https://github.com/GNOME/librsvg" rel="noopener noreferrer"&gt;librsvg &lt;/a&gt;that could be used by &lt;a href="https://github.com/rockchip-linux/ffmpeg" rel="noopener noreferrer"&gt;ffmpeg&lt;/a&gt;, but I have not been able to compile it on the Zero, and it’s just an hypothesis, there is no guarantee at all that it could be used to produce GIF files from animated SVG files.&lt;/p&gt;

&lt;p&gt;termtosvg can produce files than can be used by &lt;a href="https://github.com/asciinema/asciicast2gif?source=post_page---------------------------" rel="noopener noreferrer"&gt;asciinema &lt;/a&gt;and then asciicast2gif to produce gif files, but the process is cumbersome and does not work on the Zero. I had to use a X86 Linux machine to do it.&lt;/p&gt;

&lt;p&gt;Anyway, with this article, you won’t be able to produce GIF images starting from a terminal recording if you’re using exclusively a Zero, but beautiful animated SVG that you will be able to use on your blog, or anywhere else where animated SVG is supported.&lt;/p&gt;

&lt;p&gt;If you’re brave enough to also use an X86 machine, go to the end of this article, and take time to get married, get children before getting your first GIF file.&lt;/p&gt;

&lt;h1&gt;
  
  
  TermToSVG installation
&lt;/h1&gt;

&lt;h1&gt;
  
  
  TL;DR
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; http.proxy http://proxy-machine:proxy-port
&lt;span class="nb"&gt;sudo &lt;/span&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 1G /swapfile
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkswap /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;swapon /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/fstab
git clone https://github.com/nbedos/termtosvg.git
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;python3-setuptools libxml2-dev libxslt-dev python3-dev python3-pip
pip3 &lt;span class="nb"&gt;install &lt;/span&gt;wheel
&lt;span class="nb"&gt;cd &lt;/span&gt;termtosvg
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Let's modify the Makefile so that we use python3 and pip3 as I can't get rid of Python2.7.x on this machine..."&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"See at https://gist.github.com/gounthar/b1870915710a0437b4e4eb8690e93553#file-makefile-patch"&lt;/span&gt;
make build
pip3 &lt;span class="nb"&gt;install &lt;/span&gt;dist/termtosvg-1.1.0-py3-none-any.whl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  The real installation process
&lt;/h1&gt;

&lt;p&gt;Let’s try to install termtosvg on the Zero. First of all, let’s configure the proxy if needed and then get the source code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poddingue@orangepizero:~&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/nbedos/termtosvg.git
Cloning into &lt;span class="s1"&gt;'termtosvg'&lt;/span&gt;...
remote: Enumerating objects: 99, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Counting objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;99/99&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Compressing objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;59/59&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
remote: Total 1592 &lt;span class="o"&gt;(&lt;/span&gt;delta 59&lt;span class="o"&gt;)&lt;/span&gt;, reused 68 &lt;span class="o"&gt;(&lt;/span&gt;delta 40&lt;span class="o"&gt;)&lt;/span&gt;, pack-reused 1493
Receiving objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;1592/1592&lt;span class="o"&gt;)&lt;/span&gt;, 1.37 MiB | 2.08 MiB/s, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Resolving deltas: 100% &lt;span class="o"&gt;(&lt;/span&gt;930/930&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
poddingue@orangepizero:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;termtosvg/
poddingue@orangepizero:~/termtosvg&lt;span class="nv"&gt;$ &lt;/span&gt;make build
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; dist &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
python setup.py sdist bdist_wheel
usage: setup.py &lt;span class="o"&gt;[&lt;/span&gt;global_opts] cmd1 &lt;span class="o"&gt;[&lt;/span&gt;cmd1_opts] &lt;span class="o"&gt;[&lt;/span&gt;cmd2 &lt;span class="o"&gt;[&lt;/span&gt;cmd2_opts] ...]
   or: setup.py &lt;span class="nt"&gt;--help&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;cmd1 cmd2 ...]
   or: setup.py &lt;span class="nt"&gt;--help-commands&lt;/span&gt;
   or: setup.py cmd &lt;span class="nt"&gt;--help&lt;/span&gt;

error: invalid &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="s1"&gt;'bdist_wheel'&lt;/span&gt;
make: &lt;span class="k"&gt;***&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;Makefile:31: build] Error 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, the setuptools module can’t be found. I have not installed/configured Python2 setup tools. But as I had already done if for Python3, I modified the Makefile so that python is now python3, and pip is now pip3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;PIP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pip3
&lt;span class="nv"&gt;PYTHON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;python3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I had not done it yet, it would have looked that way:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;poddingue@orangepizero:~/termtosvg$ sudo apt-get install python3-setuptools
Reading package lists... Done
Building dependency tree
Reading state information... Done
Suggested packages:
  python-setuptools-doc
The following NEW packages will be installed:
  python3-setuptools
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 306 kB of archives.
After this operation, 1,353 kB of additional disk space will be used.
Get:1 http://httpredir.debian.org/debian buster/main armhf python3-setuptools all 40.8.0-1 [306 kB]
Fetched 306 kB in 0s (1,225 kB/s)
Selecting previously unselected package python3-setuptools.
(Reading database ... 66711 files and directories currently installed.)
Preparing to unpack .../python3-setuptools_40.8.0-1_all.deb ...
Unpacking python3-setuptools (40.8.0-1) ...
Setting up python3-setuptools (40.8.0-1) ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now that Python setup tools are installed, let’s try it again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poddingue@orangepizero:~/termtosvg&lt;span class="nv"&gt;$ &lt;/span&gt;make build
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; dist &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
python3 setup.py sdist bdist_wheel
usage: setup.py &lt;span class="o"&gt;[&lt;/span&gt;global_opts] cmd1 &lt;span class="o"&gt;[&lt;/span&gt;cmd1_opts] &lt;span class="o"&gt;[&lt;/span&gt;cmd2 &lt;span class="o"&gt;[&lt;/span&gt;cmd2_opts] ...]
   or: setup.py &lt;span class="nt"&gt;--help&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;cmd1 cmd2 ...]
   or: setup.py &lt;span class="nt"&gt;--help-commands&lt;/span&gt;
   or: setup.py cmd &lt;span class="nt"&gt;--help&lt;/span&gt;

error: invalid &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="s1"&gt;'bdist_wheel'&lt;/span&gt;
make: &lt;span class="k"&gt;***&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;Makefile:31: build] Error 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, fair enough, I’ll install wheel thanks to pip.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip3 &lt;span class="nb"&gt;install &lt;/span&gt;wheel
Collecting wheel
  Using cached https://files.pythonhosted.org/packages/00/83/b4a77d044e78ad1a45610eb88f745be2fd2c6d658f9798a15e384b7d57c9/wheel-0.33.6-py2.py3-none-any.whl
Installing collected packages: wheel
  The script wheel is installed &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;'/home/poddingue/.local/bin'&lt;/span&gt; which is not on PATH.
  Consider adding this directory to PATH or, &lt;span class="k"&gt;if &lt;/span&gt;you prefer to suppress this warning, use &lt;span class="nt"&gt;--no-warn-script-location&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Successfully installed wheel-0.33.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, as gently suggested by pip3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/poddingue/.local/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, let’s try it again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poddingue@orangepizero:~/termtosvg&lt;span class="nv"&gt;$ &lt;/span&gt;make build
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; dist &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
python3 setup.py sdist bdist_wheel
running sdist
running egg_info
creating termtosvg.egg-info
writing termtosvg.egg-info/PKG-INFO
writing dependency_links to termtosvg.egg-info/dependency_links.txt
writing requirements to termtosvg.egg-info/requires.txt
writing top-level names to termtosvg.egg-info/top_level.txt
writing manifest file &lt;span class="s1"&gt;'termtosvg.egg-info/SOURCES.txt'&lt;/span&gt;
reading manifest file &lt;span class="s1"&gt;'termtosvg.egg-info/SOURCES.txt'&lt;/span&gt;
reading manifest template &lt;span class="s1"&gt;'MANIFEST.in'&lt;/span&gt;
warning: no previously-included files matching &lt;span class="s1"&gt;'__pycache__'&lt;/span&gt; found anywhere &lt;span class="k"&gt;in &lt;/span&gt;distribution
warning: no previously-included files matching &lt;span class="s1"&gt;'*.pyc'&lt;/span&gt; found anywhere &lt;span class="k"&gt;in &lt;/span&gt;distribution
warning: no files found matching &lt;span class="s1"&gt;'man/*.man.?'&lt;/span&gt;
writing manifest file &lt;span class="s1"&gt;'termtosvg.egg-info/SOURCES.txt'&lt;/span&gt;
running check
&lt;span class="o"&gt;[&lt;/span&gt;...]
Creating &lt;span class="nb"&gt;tar &lt;/span&gt;archive
removing &lt;span class="s1"&gt;'termtosvg-1.0.0'&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;and everything under it&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;...]
running build_scripts
&lt;span class="o"&gt;[&lt;/span&gt;...]
installing to build/bdist.linux-armv7l/wheel
running &lt;span class="nb"&gt;install
&lt;/span&gt;running install_lib
&lt;span class="o"&gt;[&lt;/span&gt;...]
Copying termtosvg.egg-info to build/bdist.linux-armv7l/wheel/termtosvg-1.0.0-py3.7.egg-info
&lt;span class="o"&gt;[&lt;/span&gt;...]
adding &lt;span class="s1"&gt;'termtosvg-1.0.0.dist-info/RECORD'&lt;/span&gt;
removing build/bdist.linux-armv7l/wheel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Happy? Not really…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poddingue@orangepizero:~/termtosvg&lt;span class="nv"&gt;$ &lt;/span&gt;pip3 &lt;span class="nb"&gt;install &lt;/span&gt;dist/termtosvg-1.0.0-py3-none-any.whl
Processing ./dist/termtosvg-1.0.0-py3-none-any.whl
Collecting pyte &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;termtosvg&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.0.0&lt;span class="o"&gt;)&lt;/span&gt;
  Downloading https://files.pythonhosted.org/packages/66/37/6fed89b484c8012a0343117f085c92df8447a18af4966d25599861cd5aa0/pyte-0.8.0.tar.gz &lt;span class="o"&gt;(&lt;/span&gt;50kB&lt;span class="o"&gt;)&lt;/span&gt;
    100% |████████████████████████████████| 51kB 768kB/s
Collecting lxml &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;termtosvg&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.0.0&lt;span class="o"&gt;)&lt;/span&gt;
  Downloading https://files.pythonhosted.org/packages/e4/19/8dfeef50623892577dc05245093e090bb2bab4c8aed5cad5b03208959563/lxml-4.4.2.tar.gz &lt;span class="o"&gt;(&lt;/span&gt;2.6MB&lt;span class="o"&gt;)&lt;/span&gt;
    100% |████████████████████████████████| 2.6MB 74kB/s
Collecting wcwidth &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;termtosvg&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.0.0&lt;span class="o"&gt;)&lt;/span&gt;
  Downloading https://files.pythonhosted.org/packages/58/b4/4850a0ccc6f567cc0ebe7060d20ffd4258b8210efadc259da62dc6ed9c65/wcwidth-0.1.8-py2.py3-none-any.whl
Building wheels &lt;span class="k"&gt;for &lt;/span&gt;collected packages: pyte, lxml
  Running setup.py bdist_wheel &lt;span class="k"&gt;for &lt;/span&gt;pyte ... &lt;span class="k"&gt;done
  &lt;/span&gt;Stored &lt;span class="k"&gt;in &lt;/span&gt;directory: /home/poddingue/.cache/pip/wheels/c0/dd/4a/d0ec26b9d07a3b48e25ba3456dc9bcab875686af6da9e23fcd
  Running setup.py bdist_wheel &lt;span class="k"&gt;for &lt;/span&gt;lxml ... error
  Complete output from &lt;span class="nb"&gt;command&lt;/span&gt; /usr/bin/python3 &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import setuptools, tokenize;__file__='/tmp/pip-install-4whala87/lxml/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;', '&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;');f.close();exec(compile(code, __file__, 'exec'))"&lt;/span&gt; bdist_wheel &lt;span class="nt"&gt;-d&lt;/span&gt; /tmp/pip-wheel-w27wrikj &lt;span class="nt"&gt;--python-tag&lt;/span&gt; cp37:
  Building lxml version 4.4.2.
  Building without Cython.
  ERROR: b&lt;span class="s1"&gt;'/bin/sh: 1: xslt-config: not found\n'&lt;/span&gt;
  _ make sure the development packages of libxml2 and libxslt are installed _

  Using build configuration of libxslt
  running bdist_wheel
  running build
  running build_py
  &lt;span class="o"&gt;[&lt;/span&gt;...]
  arm-linux-gnueabihf-gcc &lt;span class="nt"&gt;-pthread&lt;/span&gt; &lt;span class="nt"&gt;-DNDEBUG&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;-fwrapv&lt;/span&gt; &lt;span class="nt"&gt;-O2&lt;/span&gt; &lt;span class="nt"&gt;-Wall&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;-fstack-protector-strong&lt;/span&gt; &lt;span class="nt"&gt;-Wformat&lt;/span&gt; &lt;span class="nt"&gt;-Werror&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;format-security &lt;span class="nt"&gt;-Wdate-time&lt;/span&gt; &lt;span class="nt"&gt;-D_FORTIFY_SOURCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 &lt;span class="nt"&gt;-fPIC&lt;/span&gt; &lt;span class="nt"&gt;-DCYTHON_CLINE_IN_TRACEBACK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nt"&gt;-Isrc&lt;/span&gt; &lt;span class="nt"&gt;-Isrc&lt;/span&gt;/lxml/includes &lt;span class="nt"&gt;-I&lt;/span&gt;/usr/include/python3.7m &lt;span class="nt"&gt;-c&lt;/span&gt; src/lxml/etree.c &lt;span class="nt"&gt;-o&lt;/span&gt; build/temp.linux-armv7l-3.7/src/lxml/etree.o &lt;span class="nt"&gt;-w&lt;/span&gt;
  In file included from src/lxml/etree.c:692:
  src/lxml/includes/etree_defs.h:14:10: fatal error: libxml/xmlversion.h: No such file or directory
   &lt;span class="c"&gt;#include "libxml/xmlversion.h"&lt;/span&gt;
            ^~~~~~~~~~~~~~~~~~~~~
  compilation terminated.
  Compile failed: &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="s1"&gt;'arm-linux-gnueabihf-gcc'&lt;/span&gt; failed with &lt;span class="nb"&gt;exit &lt;/span&gt;status 1
  creating tmp
  cc &lt;span class="nt"&gt;-I&lt;/span&gt;/usr/include/libxml2 &lt;span class="nt"&gt;-c&lt;/span&gt; /tmp/xmlXPathInitp1a2wq_x.c &lt;span class="nt"&gt;-o&lt;/span&gt; tmp/xmlXPathInitp1a2wq_x.o
  /tmp/xmlXPathInitp1a2wq_x.c:1:10: fatal error: libxml/xpath.h: No such file or directory
   &lt;span class="c"&gt;#include "libxml/xpath.h"&lt;/span&gt;
            ^~~~~~~~~~~~~~~~
  compilation terminated.
  &lt;span class="k"&gt;*********************************************************************************&lt;/span&gt;
  Could not find &lt;span class="k"&gt;function &lt;/span&gt;xmlCheckVersion &lt;span class="k"&gt;in &lt;/span&gt;library libxml2. Is libxml2 installed?
  &lt;span class="k"&gt;*********************************************************************************&lt;/span&gt;
  error: &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="s1"&gt;'arm-linux-gnueabihf-gcc'&lt;/span&gt; failed with &lt;span class="nb"&gt;exit &lt;/span&gt;status 1
  &lt;span class="o"&gt;[&lt;/span&gt;...]
Command &lt;span class="s2"&gt;"/usr/bin/python3 -u -c "&lt;/span&gt;import setuptools, tokenize&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nv"&gt;__file__&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/tmp/pip-install-4whala87/lxml/setup.py'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;getattr&lt;span class="o"&gt;(&lt;/span&gt;tokenize, &lt;span class="s1"&gt;'open'&lt;/span&gt;, open&lt;span class="o"&gt;)(&lt;/span&gt;__file__&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;f.read&lt;span class="o"&gt;()&lt;/span&gt;.replace&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'\r\n'&lt;/span&gt;, &lt;span class="s1"&gt;'\n'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;f.close&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;compile&lt;span class="o"&gt;(&lt;/span&gt;code, __file__, &lt;span class="s1"&gt;'exec'&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;" install --record /tmp/pip-record-5dypr5tk/install-record.txt --single-version-externally-managed --compile --user --prefix="&lt;/span&gt; failed with error code 1 &lt;span class="k"&gt;in&lt;/span&gt; /tmp/pip-install-4whala87/lxml/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Damn!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poddingue@orangepizero:~/termtosvg&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libxml2-dev libxslt-dev python3-dev
Reading package lists... Done
Building dependency tree
Reading state information... Done
Note, selecting &lt;span class="s1"&gt;'libxslt1-dev'&lt;/span&gt; instead of &lt;span class="s1"&gt;'libxslt-dev'&lt;/span&gt;
python3-dev is already the newest version &lt;span class="o"&gt;(&lt;/span&gt;3.7.3-1&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
The following additional packages will be installed:
  icu-devtools libicu-dev libxslt1.1
Suggested packages:
  icu-doc
The following NEW packages will be installed:
  icu-devtools libicu-dev libxml2-dev libxslt1-dev libxslt1.1
0 upgraded, 5 newly installed, 0 to remove and 0 not upgraded.
Need to get 10.5 MB of archives.
After this operation, 44.6 MB of additional disk space will be used.
Do you want to &lt;span class="k"&gt;continue&lt;/span&gt;? &lt;span class="o"&gt;[&lt;/span&gt;Y/n]
Get:1 http://httpredir.debian.org/debian buster/main armhf icu-devtools armhf 63.1-6 &lt;span class="o"&gt;[&lt;/span&gt;168 kB]
Get:2 http://httpredir.debian.org/debian buster/main armhf libicu-dev armhf 63.1-6 &lt;span class="o"&gt;[&lt;/span&gt;8,934 kB]
Get:3 http://httpredir.debian.org/debian buster/main armhf libxml2-dev armhf 2.9.4+dfsg1-7+b3 &lt;span class="o"&gt;[&lt;/span&gt;718 kB]
Get:4 http://httpredir.debian.org/debian buster/main armhf libxslt1.1 armhf 1.1.32-2.2~deb10u1 &lt;span class="o"&gt;[&lt;/span&gt;217 kB]
Get:5 http://httpredir.debian.org/debian buster/main armhf libxslt1-dev armhf 1.1.32-2.2~deb10u1 &lt;span class="o"&gt;[&lt;/span&gt;507 kB]
Fetched 10.5 MB &lt;span class="k"&gt;in &lt;/span&gt;5s &lt;span class="o"&gt;(&lt;/span&gt;2,318 kB/s&lt;span class="o"&gt;)&lt;/span&gt;
Selecting previously unselected package icu-devtools.
&lt;span class="o"&gt;(&lt;/span&gt;Reading database ... 66802 files and directories currently installed.&lt;span class="o"&gt;)&lt;/span&gt;
Preparing to unpack .../icu-devtools_63.1-6_armhf.deb ...
Unpacking icu-devtools &lt;span class="o"&gt;(&lt;/span&gt;63.1-6&lt;span class="o"&gt;)&lt;/span&gt; ...
Selecting previously unselected package libicu-dev:armhf.
Preparing to unpack .../libicu-dev_63.1-6_armhf.deb ...
Unpacking libicu-dev:armhf &lt;span class="o"&gt;(&lt;/span&gt;63.1-6&lt;span class="o"&gt;)&lt;/span&gt; ...
Selecting previously unselected package libxml2-dev:armhf.
Preparing to unpack .../libxml2-dev_2.9.4+dfsg1-7+b3_armhf.deb ...
Unpacking libxml2-dev:armhf &lt;span class="o"&gt;(&lt;/span&gt;2.9.4+dfsg1-7+b3&lt;span class="o"&gt;)&lt;/span&gt; ...
Selecting previously unselected package libxslt1.1:armhf.
Preparing to unpack .../libxslt1.1_1.1.32-2.2~deb10u1_armhf.deb ...
Unpacking libxslt1.1:armhf &lt;span class="o"&gt;(&lt;/span&gt;1.1.32-2.2~deb10u1&lt;span class="o"&gt;)&lt;/span&gt; ...
Selecting previously unselected package libxslt1-dev:armhf.
Preparing to unpack .../libxslt1-dev_1.1.32-2.2~deb10u1_armhf.deb ...
Unpacking libxslt1-dev:armhf &lt;span class="o"&gt;(&lt;/span&gt;1.1.32-2.2~deb10u1&lt;span class="o"&gt;)&lt;/span&gt; ...
Setting up icu-devtools &lt;span class="o"&gt;(&lt;/span&gt;63.1-6&lt;span class="o"&gt;)&lt;/span&gt; ...
Setting up libxslt1.1:armhf &lt;span class="o"&gt;(&lt;/span&gt;1.1.32-2.2~deb10u1&lt;span class="o"&gt;)&lt;/span&gt; ...
Setting up libicu-dev:armhf &lt;span class="o"&gt;(&lt;/span&gt;63.1-6&lt;span class="o"&gt;)&lt;/span&gt; ...
Setting up libxml2-dev:armhf &lt;span class="o"&gt;(&lt;/span&gt;2.9.4+dfsg1-7+b3&lt;span class="o"&gt;)&lt;/span&gt; ...
Setting up libxslt1-dev:armhf &lt;span class="o"&gt;(&lt;/span&gt;1.1.32-2.2~deb10u1&lt;span class="o"&gt;)&lt;/span&gt; ...
Processing triggers &lt;span class="k"&gt;for &lt;/span&gt;man-db &lt;span class="o"&gt;(&lt;/span&gt;2.8.5-2&lt;span class="o"&gt;)&lt;/span&gt; ...
Processing triggers &lt;span class="k"&gt;for &lt;/span&gt;libc-bin &lt;span class="o"&gt;(&lt;/span&gt;2.28-10&lt;span class="o"&gt;)&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All set? Not so sure…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Command &lt;span class="s2"&gt;"/usr/bin/python3 -u -c "&lt;/span&gt;import setuptools, tokenize&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nv"&gt;__file__&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/tmp/pip-install-9lgddbtt/lxml/setup.py'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;getattr&lt;span class="o"&gt;(&lt;/span&gt;tokenize, &lt;span class="s1"&gt;'open'&lt;/span&gt;, open&lt;span class="o"&gt;)(&lt;/span&gt;__file__&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;f.read&lt;span class="o"&gt;()&lt;/span&gt;.replace&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'\r\n'&lt;/span&gt;, &lt;span class="s1"&gt;'\n'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;f.close&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;compile&lt;span class="o"&gt;(&lt;/span&gt;code, __file__, &lt;span class="s1"&gt;'exec'&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;" install --record /tmp/pip-record-ghyj96g8/install-record.txt --single-version-externally-managed --compile --user --prefix="&lt;/span&gt; failed with error code 1 &lt;span class="k"&gt;in&lt;/span&gt; /tmp/pip-install-9lgddbtt/lxml/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error may have happened because of low memory. Let’s this machine breathe a little better by giving it more swap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 1G /swapfile
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkswap /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;swapon /swapfile
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let’s add this line in &lt;code&gt;/etc/fstab&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/swapfile swap swap defaults 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s try to install it once again. Go and have a coffee, or go for a walk…​ You could maybe even take time to marry and start a family before the build finishes. Anyway, after some time, you’ll have your beautiful termtosvg package ready to rock!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip3 &lt;span class="nb"&gt;install &lt;/span&gt;dist/termtosvg-1.0.0-py3-none-any.whl
Processing ./dist/termtosvg-1.0.0-py3-none-any.whl
Collecting lxml &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;termtosvg&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.0.0&lt;span class="o"&gt;)&lt;/span&gt;
  Using cached https://files.pythonhosted.org/packages/e4/19/8dfeef50623892577dc05245093e090bb2bab4c8aed5cad5b03208959563/lxml
Requirement already satisfied: pyte &lt;span class="k"&gt;in&lt;/span&gt; /home/poddingue/.local/lib/python3.7/site-packages &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;termtosvg&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.0.0&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0.8.0&lt;span class="o"&gt;)&lt;/span&gt;
Requirement already satisfied: wcwidth &lt;span class="k"&gt;in&lt;/span&gt; /home/poddingue/.local/lib/python3.7/site-packages &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;termtosvg&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.0.0&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0.1.8&lt;span class="o"&gt;)&lt;/span&gt;
Building wheels &lt;span class="k"&gt;for &lt;/span&gt;collected packages: lxml
  Running setup.py bdist_wheel &lt;span class="k"&gt;for &lt;/span&gt;lxml ... -


&lt;span class="k"&gt;done
  &lt;/span&gt;Stored &lt;span class="k"&gt;in &lt;/span&gt;directory: /home/poddingue/.cache/pip/wheels/73/52/7c/5cd696851d3e5e31a05023dd402b04659a6ec695ecc566c9d3
Successfully built lxml
Installing collected packages: lxml, termtosvg
Successfully installed lxml-4.4.2 termtosvg-1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, it looks like our installation has been successful. Let’s try it for real now.&lt;/p&gt;

&lt;h1&gt;
  
  
  Testing
&lt;/h1&gt;

&lt;p&gt;Just enter termtosvg, do your stuff, and then do exit. You will have a message that will tell you where your newly created file lies.&lt;/p&gt;

&lt;p&gt;That’s cool, but can we do any better? The man &lt;a href="https://github.com/nbedos/termtosvg/blob/develop/man/termtosvg.md" rel="noopener noreferrer"&gt;page&lt;/a&gt; tells us we can record a terminal session with a specific screen geometry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;termtosvg &lt;span class="nt"&gt;-g&lt;/span&gt; 80x24 animation.svg
Recording started, enter &lt;span class="s2"&gt;"exit"&lt;/span&gt; &lt;span class="nb"&gt;command &lt;/span&gt;or Control-D to end
poddingue@orangepizero:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello, World!"&lt;/span&gt;
Hello, World!
poddingue@orangepizero:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;exit
&lt;/span&gt;Rendering ended, SVG animation is animation.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty handy!&lt;/p&gt;

&lt;p&gt;I am recording my casts with a 200x60 geometry, exporting them in the SVG format. The first step is to record them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;termtosvg record &lt;span class="nt"&gt;-g&lt;/span&gt; 200x60 whatever-name-I-choose.cast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then ask termtosvg to render the cast as SVG, so that I can use them in websites that can handle them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;termtosvg render whatever-name-I-choose.cast whatever-name-I-choose.svg
Rendering started
Rendering ended, SVG animation is whatever-name-I-choose.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want more? You’re GIF hungry? Ok I resign myself to give you my process, which is not pretty, and requires an X86 machine. As we have seen before, we can record a terminal session in asciicast v2 format. The cast file we have produced during the previous step to generate SVG, can then be used by &lt;a href="https://github.com/asciinema/asciicast2gif?source=post_page---------------------------" rel="noopener noreferrer"&gt;asciinema&lt;/a&gt; (using &lt;a href="https://phantomjs.org/" rel="noopener noreferrer"&gt;phantomjs&lt;/a&gt; in the background) to produce a GIF file.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker pull asciinema/asciicast2gif
alias asciicast2gif='docker run --rm -v $PWD:/data asciinema/asciicast2gif'
asciicast2gif -s 2 -t solarized-dark whatever-name-I-choose.cast demo.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And here is the final result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2Fwhatever-name-I-choose.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2Fwhatever-name-I-choose.svg" alt="whatever-name-I-choose" width="1600" height="1022.0"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And in GIF for the CMS that can’t display properly SVG.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2Fwhatever-name-I-choose.cast.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2Fassets%2Fimages%2Fwhatever-name-I-choose.cast.gif" alt="whatever-name-I-choose.cast" width="2912" height="1944"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>armbian</category>
      <category>h3</category>
      <category>termtosvg</category>
    </item>
    <item>
      <title>Sphinx: Installing sphinxbase on the OrangePi 4B</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Wed, 22 Apr 2026 07:44:21 +0000</pubDate>
      <link>https://dev.to/gounthar/sphinx-installing-sphinxbase-on-the-orangepi-4b-223e</link>
      <guid>https://dev.to/gounthar/sphinx-installing-sphinxbase-on-the-orangepi-4b-223e</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on June 5, 2020 at &lt;a href="https://bruno.verachten.fr/2020/06/05/sphinx-installing-sphinxbase-on-the-orangepi-4b/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2020/06/05/sphinx-installing-sphinxbase-on-the-orangepi-4b/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;This post will be part of a series regarding generating subtitles for videos with the &lt;a href="http://www.orangepi.org/Orange%20Pi%204B/" rel="noopener noreferrer"&gt;OrangePi 4B&lt;/a&gt; running &lt;a href="https://www.armbian.com/orange-pi-4/" rel="noopener noreferrer"&gt;Armbian&lt;/a&gt;. We’re not yet sure of what we’ll use to generate those subtitles, so for the time being, we’ll use &lt;a href="https://cmusphinx.github.io/wiki/tutorialadapt/" rel="noopener noreferrer"&gt;Sphinx&lt;/a&gt;, and in the following months, we’ll try to setup something that will take advantage of the &lt;a href="https://drive.google.com/drive/folders/1QMU5n-7fCx28cQPH_lu2L0wrFWeC3G45" rel="noopener noreferrer"&gt;NPU&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Today, we’ll install SphinxBase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/cmusphinx/sphinxbase.git
&lt;span class="nb"&gt;cd &lt;/span&gt;sphinxbase/
./autogen.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately we get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;_Error_: You must have &lt;span class="sb"&gt;`&lt;/span&gt;libtool&lt;span class="s1"&gt;' installed.
Get ftp://ftp.gnu.org/pub/gnu/libtool/libtool-2.2.6b.tar.gz
(or a newer version if it is available)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, my bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libtool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s give it another try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./autogen.sh
&lt;span class="o"&gt;[&lt;/span&gt;...]
configure: error: &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;/home/poddingue/sphinxbase&lt;span class="s1"&gt;':
configure: error:
  Could not link test program to Python. Maybe the main Python library has been
  installed in some non-standard library path. If so, pass it to configure,
  via the LDFLAGS environment variable.
  Example: ./configure LDFLAGS="-L/usr/non-standard-path/python/lib"
  ============================================================================
   ERROR!
   You probably have to install the development version of the Python package
   for your distribution.  The exact name of this package varies among them.
  ============================================================================

See `config.log'&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;more details
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I see. I may have forgotten to install Python-dev…​&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;python3-dev
&lt;span class="o"&gt;[&lt;/span&gt;...]
./autogen.sh
&lt;span class="o"&gt;[&lt;/span&gt;...]
configure: error: &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;/home/poddingue/sphinxbase&lt;span class="s1"&gt;':
configure: error:
  Could not link test program to Python. Maybe the main Python library has been
  installed in some non-standard library path. If so, pass it to configure,
  via the LDFLAGS environment variable.
  Example: ./configure LDFLAGS="-L/usr/non-standard-path/python/lib"
  ============================================================================
   ERROR!
   You probably have to install the development version of the Python package
   for your distribution.  The exact name of this package varies among them.
  ============================================================================
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How come? Are you still working with Python 2.x? Naaaah…​ Ok, you never know, let’s try it this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;python-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, that was it. Now, let’s tackle this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;checking python extra linking flags... &lt;span class="nt"&gt;-Xlinker&lt;/span&gt; &lt;span class="nt"&gt;-export-dynamic&lt;/span&gt; &lt;span class="nt"&gt;-Wl&lt;/span&gt;,-O1 &lt;span class="nt"&gt;-Wl&lt;/span&gt;,-Bsymbolic-functions
checking consistency of all components of python development environment... &lt;span class="nb"&gt;yes
&lt;/span&gt;checking &lt;span class="k"&gt;for &lt;/span&gt;swig... no
checking &lt;span class="k"&gt;for &lt;/span&gt;swig2.0... no
configure: error: swig not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should be solved by&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;swig
./autogen.sh
&lt;span class="o"&gt;[&lt;/span&gt;...]
config.status: executing libtool commands
Now &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;make&lt;span class="s1"&gt;' to compile the package.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bingo! Time for a good old&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make &lt;span class="nt"&gt;-j6&lt;/span&gt;
make check
&lt;span class="o"&gt;[&lt;/span&gt;...]
Testsuite summary &lt;span class="k"&gt;for &lt;/span&gt;sphinxbase 5prealpha
&lt;span class="o"&gt;============================================================================&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; TOTAL: 10
&lt;span class="o"&gt;==&lt;/span&gt; PASS:  10
&lt;span class="o"&gt;==&lt;/span&gt; SKIP:  0
&lt;span class="o"&gt;==&lt;/span&gt; XFAIL: 0
&lt;span class="o"&gt;==&lt;/span&gt; FAIL:  0
&lt;span class="o"&gt;==&lt;/span&gt; XPASS: 0
&lt;span class="o"&gt;==&lt;/span&gt; ERROR: 0
&lt;span class="o"&gt;============================================================================&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All went fine. Let’s install it then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Next step, install Pocket Sphinx.&lt;/p&gt;

</description>
      <category>armbian</category>
      <category>machinelearning</category>
      <category>merrykombo</category>
      <category>rk3399</category>
    </item>
    <item>
      <title>Installing TensorFlow on the Orange Pi 4B</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Wed, 22 Apr 2026 07:44:19 +0000</pubDate>
      <link>https://dev.to/gounthar/installing-tensorflow-on-the-orange-pi-4b-e60</link>
      <guid>https://dev.to/gounthar/installing-tensorflow-on-the-orange-pi-4b-e60</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on June 19, 2020 at &lt;a href="https://bruno.verachten.fr/2020/06/19/installing-tensorflow-on-the-orange-pi-4b/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2020/06/19/installing-tensorflow-on-the-orange-pi-4b/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;This quick howto is heavily based on Orange Pi’s &lt;a href="http://www.orangepi.org/downloadresources/OrangePi4B/2020-01-03/orangepi3_46708b05ab3339f316305414ee.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. To this day, &lt;a href="http://www.orangepi.org/" rel="noopener noreferrer"&gt;Orange Pi&lt;/a&gt; recommended versions of &lt;a href="https://www.tensorflow.org/" rel="noopener noreferrer"&gt;TensorFlow&lt;/a&gt; and &lt;a href="https://www.python.org/downloads/" rel="noopener noreferrer"&gt;Python&lt;/a&gt; are quite old:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TensorFlow&lt;/em&gt;：1.14.0 &lt;em&gt;Python environment&lt;/em&gt;：python2.7&lt;/p&gt;

&lt;h1&gt;
  
  
  Installation of Python environment and dependencies
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;python-pip python-dev libatlas-base-dev
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; password &lt;span class="k"&gt;for &lt;/span&gt;poddingue:
Reading package lists... Done
Building dependency tree
Reading state information... Done
python-dev is already the newest version &lt;span class="o"&gt;(&lt;/span&gt;2.7.16-1&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
The following additional packages will be installed:
  libatlas3-base python-pip-whl
Suggested packages:
  libatlas-doc liblapack-doc
Recommended packages:
  python-all-dev python-setuptools python-wheel
The following NEW packages will be installed:
  libatlas-base-dev libatlas3-base python-pip python-pip-whl
Need to get 7,168 kB of archives.
After this operation, 40.1 MB of additional disk space will be used.
Do you want to &lt;span class="k"&gt;continue&lt;/span&gt;? &lt;span class="o"&gt;[&lt;/span&gt;Y/n]
Get:1 http://debian.proxad.net/debian buster/main arm64 libatlas3-base arm64 3.10.3-8 &lt;span class="o"&gt;[&lt;/span&gt;2,409 kB]
Get:2 http://debian.proxad.net/debian buster/main arm64 libatlas-base-dev arm64 3.10.3-8 &lt;span class="o"&gt;[&lt;/span&gt;2,954 kB]
Get:3 http://debian.proxad.net/debian buster/main arm64 python-pip-whl all 18.1-5 &lt;span class="o"&gt;[&lt;/span&gt;1,591 kB]
Get:4 http://debian.proxad.net/debian buster/main arm64 python-pip all 18.1-5 &lt;span class="o"&gt;[&lt;/span&gt;215 kB]
Fetched 7,168 kB &lt;span class="k"&gt;in &lt;/span&gt;1s &lt;span class="o"&gt;(&lt;/span&gt;8,178 kB/s&lt;span class="o"&gt;)&lt;/span&gt;
Selecting previously unselected package libatlas3-base:arm64.
&lt;span class="o"&gt;(&lt;/span&gt;Reading database ... 112671 files and directories currently installed.&lt;span class="o"&gt;)&lt;/span&gt;
Preparing to unpack .../libatlas3-base_3.10.3-8_arm64.deb ...
Unpacking libatlas3-base:arm64 &lt;span class="o"&gt;(&lt;/span&gt;3.10.3-8&lt;span class="o"&gt;)&lt;/span&gt; ...
Selecting previously unselected package libatlas-base-dev:arm64.
Preparing to unpack .../libatlas-base-dev_3.10.3-8_arm64.deb ...
Unpacking libatlas-base-dev:arm64 &lt;span class="o"&gt;(&lt;/span&gt;3.10.3-8&lt;span class="o"&gt;)&lt;/span&gt; ...
Selecting previously unselected package python-pip-whl.
Preparing to unpack .../python-pip-whl_18.1-5_all.deb ...
Unpacking python-pip-whl &lt;span class="o"&gt;(&lt;/span&gt;18.1-5&lt;span class="o"&gt;)&lt;/span&gt; ...
Selecting previously unselected package python-pip.
Preparing to unpack .../python-pip_18.1-5_all.deb ...
Unpacking python-pip &lt;span class="o"&gt;(&lt;/span&gt;18.1-5&lt;span class="o"&gt;)&lt;/span&gt; ...
Setting up libatlas3-base:arm64 &lt;span class="o"&gt;(&lt;/span&gt;3.10.3-8&lt;span class="o"&gt;)&lt;/span&gt; ...
update-alternatives: using /usr/lib/aarch64-linux-gnu/atlas/libblas.so.3 to provide /usr/lib/aarch64-linux-gnu/libblas.so.3 &lt;span class="o"&gt;(&lt;/span&gt;libblas.so.3-aarch64-linux-gnu&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;auto mode
update-alternatives: using /usr/lib/aarch64-linux-gnu/atlas/liblapack.so.3 to provide /usr/lib/aarch64-linux-gnu/liblapack.so.3 &lt;span class="o"&gt;(&lt;/span&gt;liblapack.so.3-aarch64-linux-gnu&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;auto mode
Setting up libatlas-base-dev:arm64 &lt;span class="o"&gt;(&lt;/span&gt;3.10.3-8&lt;span class="o"&gt;)&lt;/span&gt; ...
update-alternatives: using /usr/lib/aarch64-linux-gnu/atlas/libblas.so to provide /usr/lib/aarch64-linux-gnu/libblas.so &lt;span class="o"&gt;(&lt;/span&gt;libblas.so-aarch64-linux-gnu&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;auto mode
update-alternatives: using /usr/lib/aarch64-linux-gnu/atlas/liblapack.so to provide /usr/lib/aarch64-linux-gnu/liblapack.so &lt;span class="o"&gt;(&lt;/span&gt;liblapack.so-aarch64-linux-gnu&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;auto mode
Setting up python-pip-whl &lt;span class="o"&gt;(&lt;/span&gt;18.1-5&lt;span class="o"&gt;)&lt;/span&gt; ...
Setting up python-pip &lt;span class="o"&gt;(&lt;/span&gt;18.1-5&lt;span class="o"&gt;)&lt;/span&gt; ...
Processing triggers &lt;span class="k"&gt;for &lt;/span&gt;man-db &lt;span class="o"&gt;(&lt;/span&gt;2.8.5-2&lt;span class="o"&gt;)&lt;/span&gt; ...
Processing triggers &lt;span class="k"&gt;for &lt;/span&gt;libc-bin &lt;span class="o"&gt;(&lt;/span&gt;2.28-10&lt;span class="o"&gt;)&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Install TensorFlow
&lt;/h1&gt;

&lt;p&gt;There are several ways to install TensorFlow, but here we will install if from a downloaded binary. Download the PIP installation package for TensorFlow from the following page: &lt;a href="https://github.com/lhelontra/tensorflow-on-arm/releases/tag/v1.14.0" rel="noopener noreferrer"&gt;https://github.com/lhelontra/tensorflow-on-arm/releases/tag/v1.14.0&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;This should give something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://github.com/lhelontra/tensorflow-on-arm/releases/download/v1.14.0/tensorflow-1.14.0-cp27-none-linux_aarch64.whl"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tensorflow-1.14.0-cp27-none-linux_aarch64.whl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let’s use PIP to install TensorFlow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;tensorflow-1.14.0-cp27-none-linux_aarch64.whl
Processing ./tensorflow-1.14.0-cp27-none-linux_aarch64.whl
Collecting keras-preprocessing&amp;gt;&lt;span class="o"&gt;=&lt;/span&gt;1.0.5 &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;tensorflow&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.14.0&lt;span class="o"&gt;)&lt;/span&gt;
  Downloading https://files.pythonhosted.org/packages/79/4c/7c3275a01e12ef9368a892926ab932b33bb13d55794881e3573482b378a7/Keras_Preprocessing-1.1.2-py2.py3-none-any.whl &lt;span class="o"&gt;(&lt;/span&gt;42kB&lt;span class="o"&gt;)&lt;/span&gt;
    100% |████████████████████████████████| 51kB 2.5MB/s
Collecting enum34&amp;gt;&lt;span class="o"&gt;=&lt;/span&gt;1.1.6 &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;tensorflow&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.14.0&lt;span class="o"&gt;)&lt;/span&gt;
  Downloading https://files.pythonhosted.org/packages/6f/2c/a9386903ece2ea85e9807e0e062174dc26fdce8b05f216d00491be29fad5/enum34-1.1.10-py2-none-any.whl
Collecting wheel &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;tensorflow&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.14.0&lt;span class="o"&gt;)&lt;/span&gt;
  Downloading https://files.pythonhosted.org/packages/8c/23/848298cccf8e40f5bbb59009b32848a4c38f4e7f3364297ab3c3e2e2cd14/wheel-0.34.2-py2.py3-none-any.whl
Collecting wrapt&amp;gt;&lt;span class="o"&gt;=&lt;/span&gt;1.11.1 &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="nv"&gt;tensorflow&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.14.0&lt;span class="o"&gt;)&lt;/span&gt;
  Downloading https://files.pythonhosted.org/packages/82/f7/e43cefbe88c5fd371f4cf0cf5eb3feccd07515af9fd6cf7dbf1d1793a797/wrapt-1.12.1.tar.gz
    Complete output from &lt;span class="nb"&gt;command &lt;/span&gt;python setup.py egg_info:
    Traceback &lt;span class="o"&gt;(&lt;/span&gt;most recent call last&lt;span class="o"&gt;)&lt;/span&gt;:
      File &lt;span class="s2"&gt;"&amp;lt;string&amp;gt;"&lt;/span&gt;, line 1, &lt;span class="k"&gt;in&lt;/span&gt; &amp;lt;module&amp;gt;
    ImportError: No module named setuptools
&lt;span class="nt"&gt;----------------------------------------&lt;/span&gt;
Command &lt;span class="s2"&gt;"python setup.py egg_info"&lt;/span&gt; failed with error code 1 &lt;span class="k"&gt;in&lt;/span&gt; /tmp/pip-install-Dd0045/wrapt/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course…​&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python-setuptools
&lt;span class="o"&gt;[&lt;/span&gt;...]
  error: invalid &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="s1"&gt;'bdist_wheel'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Damn!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;wheel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;:/home/poddingue/.local/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;in `~/.bashrc`, because of
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;wheel
Collecting wheel
  Using cached https://files.pythonhosted.org/packages/8c/23/848298cccf8e40f5bbb59009b32848a4c38f4e7f3364297ab3c3e2e2cd14/wheel-0.34.2-py2.py3-none-any.whl
Installing collected packages: wheel
  The script wheel is installed &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;'/home/poddingue/.local/bin'&lt;/span&gt; which is not on PATH.
  Consider adding this directory to PATH or, &lt;span class="k"&gt;if &lt;/span&gt;you prefer to suppress this warning, use &lt;span class="nt"&gt;--no-warn-script-location&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Successfully installed wheel-0.34.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s issue once more `&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;` bash&lt;br&gt;
pip install tensorflow-1.14.0-cp27-none-linux_aarch64.whl&lt;/p&gt;

&lt;p&gt;​    error: libhdf5.so: cannot open shared object file: No such file or directory&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Bad luck.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
Command "/usr/bin/python -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-irkqLB/h5py/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-record-N9oIr_/install-record.txt --single-version-externally-managed --compile --user --prefix=" failed with error code 1 in /tmp/pip-install-irkqLB/h5py/&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Something must be missing…​&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
Command "/usr/bin/python -u -c "import setuptools, tokenize;__file__='/tmp/pip-install-irkqLB/h5py/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record /tmp/pip-record-N9oIr_/install-record.txt --single-version-externally-managed --compile --user --prefix=" failed with error code 1 in /tmp/pip-install-irkqLB/h5py/&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Not better…​ I think the machine is trying to tell me something…​ A missing library perhaps?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
sudo apt install python-h5py&lt;br&gt;
pip install tensorflow-1.14.0-cp27-none-linux_aarch64.whl&lt;br&gt;
[...]&lt;br&gt;
Successfully installed astor-0.8.1 backports.weakref-1.0.post1 funcsigs-1.0.2 gast-0.3.3 google-pasta-0.2.0 keras-applications-1.0.8 mock-3.0.5 tensorflow-1.14.0 tensorflow-estimator-1.14.0 termcolor-1.1.0&lt;br&gt;
rm tensorflow-1.14.0-cp27-none-linux_aarch64.whl&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It worked!&lt;/p&gt;

&lt;p&gt;Now, let’s edit a test.py file:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;` python&lt;br&gt;
import tensorflow as tf&lt;br&gt;
hello = tf.constant('Hello, TensorFlow!')&lt;/p&gt;

&lt;h1&gt;
  
  
  sess = tf.Session()
&lt;/h1&gt;

&lt;p&gt;sess = tf.compat.v1.Session()&lt;br&gt;
print(sess.run(hello))&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
python test.py&lt;br&gt;
Hello, TensorFlow!&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We all have to start somewhere, don’t we?&lt;/p&gt;

&lt;h1&gt;
  
  
  TL;DR
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
sudo apt-get install -y python-setuptools python-h5py python-pip python-dev libatlas-base-dev&lt;br&gt;
curl -L "https://github.com/lhelontra/tensorflow-on-arm/releases/download/v1.14.0/tensorflow-1.14.0-cp27-none-linux_aarch64.whl" --output tensorflow-1.14.0-cp27-none-linux_aarch64.whll&lt;br&gt;
pip install wheel&lt;br&gt;
pip install tensorflow-1.14.0-cp27-none-linux_aarch64.whl&lt;br&gt;
cat &amp;gt;&amp;gt; ~/.bashrc&lt;br&gt;
  export PATH=$PATH:/home/poddingue/.local/bin&lt;br&gt;
  CTRL+D&lt;br&gt;
source ~/.bashrc&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

</description>
      <category>4b</category>
      <category>armbian</category>
      <category>machinelearning</category>
      <category>merrykombo</category>
    </item>
    <item>
      <title>Installing Caffe on the Orange Pi 4B</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Wed, 22 Apr 2026 07:44:17 +0000</pubDate>
      <link>https://dev.to/gounthar/installing-caffe-on-the-orange-pi-4b-5a1</link>
      <guid>https://dev.to/gounthar/installing-caffe-on-the-orange-pi-4b-5a1</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on June 22, 2020 at &lt;a href="https://bruno.verachten.fr/2020/06/22/installing-caffe-on-the-orange-pi-4b/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2020/06/22/installing-caffe-on-the-orange-pi-4b/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Install dependencies
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;libprotobuf-dev libleveldb-dev libsnappy-dev
libopencv-dev libhdf5-serial-dev protobuf-compiler cmake
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; libboost-all-dev
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;libopenblas-dev liblapack-dev libatlas-base-dev
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;libgflags-dev libgoogle-glog-dev liblmdb-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Download Caffe and modify configuration parameters
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/BVLC/caffe
&lt;span class="nb"&gt;cd &lt;/span&gt;caffe
&lt;span class="nb"&gt;cp &lt;/span&gt;Makefile.config.example Makefile.config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modify the parameters of Makefile.config Change &lt;code&gt;#CPU_ONLY := 1&lt;/code&gt; into &lt;code&gt;CPU_ONLY := 1&lt;/code&gt; Change &lt;code&gt;＃OPENCV_VERSION := 3&lt;/code&gt; into &lt;code&gt;OPENCV_VERSION := 3&lt;/code&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Install Caffe
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;build
&lt;span class="nb"&gt;cd &lt;/span&gt;build
cmake ..
  Protobuf compiler version doesn&lt;span class="s1"&gt;'t match library version 3.6.1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay…​ Let’s try a &lt;code&gt;sudo apt install protobuf-compiler&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;protoc &lt;span class="nt"&gt;--version&lt;/span&gt;
libprotoc 3.6.1
cmake ..
  Could NOT find HDF5 &lt;span class="o"&gt;(&lt;/span&gt;missing: HDF5_LIBRARIES HDF5_INCLUDE_DIRS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My bad, let’s install it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libhdf5-dev
cmake ..
  Could not find a package configuration file provided by &lt;span class="s2"&gt;"OpenCV"&lt;/span&gt; with any
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s install openCV, then…​&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libopencv-dev
cmake ..
&lt;span class="o"&gt;[&lt;/span&gt;...]
&lt;span class="nt"&gt;--&lt;/span&gt; Configuring &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="nt"&gt;--&lt;/span&gt; Generating &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="nt"&gt;--&lt;/span&gt; Build files have been written to: /home/poddingue/caffe/build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next step, let’s build it for real. By the way, kiddoes, don’t try this &lt;code&gt;-j6&lt;/code&gt; at home unless you have a good heatsink…​ Or the machine will stop/hang up/reboot without prior alert.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make all &lt;span class="nt"&gt;-j6&lt;/span&gt;
make pycaffe &lt;span class="nt"&gt;-j6&lt;/span&gt;
make &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-j6&lt;/span&gt;
make runtest &lt;span class="nt"&gt;-j6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Run a simple example
&lt;/h1&gt;

&lt;p&gt;Run the caffe example — mnist instance: Mnist is a handwritten digital library. Originally used for handwritten numeral recognition on checks, and it is now the DL’s starting exercise library. The special model for mnist identification is Lenet, which is the earliest CNN model. The training samples of mnist data are 60,000 units, and the test samples are 10,000 units. Each sample is a black and white picture of size 28*28, and the handwritten number is 0-9, so it is divided into 10 categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Download the mnist data first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sh data/mnist/get_mnist.sh
&lt;span class="nb"&gt;cd &lt;/span&gt;data/mnist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are four files in the directory after running the previous command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ll
total 53676
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue      408 Jun 22 16:55 get_mnist.sh
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue  7840016 Jul 21  2000 t10k-images-idx3-ubyte &lt;span class="c"&gt;# Training set samples&lt;/span&gt;
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue    10008 Jul 21  2000 t10k-labels-idx1-ubyte &lt;span class="c"&gt;# Corresponding annotation of training set&lt;/span&gt;
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue 47040016 Jul 21  2000 train-images-idx3-ubyte &lt;span class="c"&gt;# Testing set sample&lt;/span&gt;
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue    60008 Jul 21  2000 train-labels-idx1-ubyte &lt;span class="c"&gt;# Corresponding annotation of testing set&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Converting the data
&lt;/h2&gt;

&lt;p&gt;These data cannot be used directly in Caffe, and need to be converted into LMDB data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../..
sh examples/mnist/create_mnist.sh
Creating lmdb...
I0622 17:37:06.109530 12691 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_train_lmdb
I0622 17:37:06.114603 12691 convert_mnist_data.cpp:88] A total of 60000 items.
I0622 17:37:06.114666 12691 convert_mnist_data.cpp:89] Rows: 28 Cols: 28
I0622 17:37:11.407480 12691 convert_mnist_data.cpp:108] Processed 60000 files.
I0622 17:37:12.192065 12698 db_lmdb.cpp:35] Opened lmdb examples/mnist/mnist_test_lmdb
I0622 17:37:12.194784 12698 convert_mnist_data.cpp:88] A total of 10000 items.
I0622 17:37:12.198009 12698 convert_mnist_data.cpp:89] Rows: 28 Cols: 28
I0622 17:37:12.566424 12698 convert_mnist_data.cpp:108] Processed 10000 files.
Done.
ll examples/mnist/
total 136
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue  4520 Jun 22 16:55 convert_mnist_data.cpp
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue   634 Jun 22 16:55 create_mnist.sh
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   777 Jun 22 16:55 lenet_adadelta_solver.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   778 Jun 22 16:55 lenet_auto_solver.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue  6003 Jun 22 16:55 lenet_consolidated_solver.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   871 Jun 22 16:55 lenet_multistep_solver.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue  1738 Jun 22 16:55 lenet.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   886 Jun 22 16:55 lenet_solver_adam.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   790 Jun 22 16:55 lenet_solver.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   830 Jun 22 16:55 lenet_solver_rmsprop.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue  2282 Jun 22 16:55 lenet_train_test.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue  4814 Jun 22 16:55 mnist_autoencoder.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   451 Jun 22 16:55 mnist_autoencoder_solver_adadelta.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   423 Jun 22 16:55 mnist_autoencoder_solver_adagrad.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   466 Jun 22 16:55 mnist_autoencoder_solver_nesterov.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   433 Jun 22 16:55 mnist_autoencoder_solver.prototxt
drwxr--r-- 2 poddingue poddingue  4096 Jun 22 17:37 mnist_test_lmdb
drwxr--r-- 2 poddingue poddingue  4096 Jun 22 17:37 mnist_train_lmdb
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue 11948 Jun 22 16:55 readme.md
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue   106 Jun 22 16:55 train_lenet_adam.sh
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue   118 Jun 22 16:55 train_lenet_consolidated.sh
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue  4517 Jun 22 16:55 train_lenet_docker.sh
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue   115 Jun 22 16:55 train_lenet_rmsprop.sh
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue   101 Jun 22 16:55 train_lenet.sh
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue   120 Jun 22 16:55 train_mnist_autoencoder_adadelta.sh
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue   119 Jun 22 16:55 train_mnist_autoencoder_adagrad.sh
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue   120 Jun 22 16:55 train_mnist_autoencoder_nesterov.sh
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue   117 Jun 22 16:55 train_mnist_autoencoder.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;examples/mnist/create_mnist.sh&lt;/code&gt; has generated two folders in the &lt;code&gt;examples/mnist/&lt;/code&gt; directory, namely &lt;code&gt;mnist_train_lmdb&lt;/code&gt; and &lt;code&gt;mnist_test_lmdb&lt;/code&gt; after the LMDB data conversion succeeded. The &lt;code&gt;data.mdb&lt;/code&gt; and &lt;code&gt;lock.mdb&lt;/code&gt; stored in the folder are the running data we need.&lt;/p&gt;

&lt;h2&gt;
  
  
  LevelDB Data
&lt;/h2&gt;

&lt;p&gt;If you want to run leveldb data, run the program under &lt;code&gt;examples/siamese/&lt;/code&gt; folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; ./examples/siamese/create_mnist_siamese.sh
Creating leveldb...
Done.
ll examples/siamese/
total 204
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   4368 Jun 22 16:55 convert_mnist_siamese_data.cpp
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue    617 Jun 22 16:55 create_mnist_siamese.sh
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue 158921 Jun 22 16:55 mnist_siamese.ipynb
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   1488 Jun 22 16:55 mnist_siamese.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue    810 Jun 22 16:55 mnist_siamese_solver.prototxt
drwxr-xr-x 2 poddingue poddingue   4096 Jun 22 17:39 mnist_siamese_test_leveldb
drwxr-xr-x 2 poddingue poddingue   4096 Jun 22 17:39 mnist_siamese_train_leveldb
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   4907 Jun 22 16:55 mnist_siamese_train_test.prototxt
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 poddingue poddingue   5949 Jun 22 16:55 readme.md
&lt;span class="nt"&gt;-rwxr-xr-x&lt;/span&gt; 1 poddingue poddingue    125 Jun 22 16:55 train_mnist_siamese.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Next modify the configuration file lenet_solver.prototxt
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vi ./examples/mnist/lenet_solver.prototxt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As needed, set the maximum number of iterations at 100 and the last line, &lt;code&gt;solver_mode&lt;/code&gt;, to &lt;code&gt;CPU&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;==&lt;/span&gt; The train/test net protocol buffer definition
net: &lt;span class="s2"&gt;"examples/mnist/lenet_train_test.prototxt"&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; test_iter specifies how many forward passes the &lt;span class="nb"&gt;test &lt;/span&gt;should carry out.
&lt;span class="o"&gt;==&lt;/span&gt; In the &lt;span class="k"&gt;case&lt;/span&gt; of MNIST, we have &lt;span class="nb"&gt;test &lt;/span&gt;batch size 100 and 100 &lt;span class="nb"&gt;test &lt;/span&gt;iterations,
&lt;span class="o"&gt;==&lt;/span&gt; covering the full 10,000 testing images.
test_iter: 100
&lt;span class="o"&gt;==&lt;/span&gt; Carry out testing every 500 training iterations.
test_interval: 500
&lt;span class="o"&gt;==&lt;/span&gt; The base learning rate, momentum and the weight decay of the network.
base_lr: 0.01
momentum: 0.9
weight_decay: 0.0005
&lt;span class="o"&gt;==&lt;/span&gt; The learning rate policy
lr_policy: &lt;span class="s2"&gt;"inv"&lt;/span&gt;
gamma: 0.0001
power: 0.75
&lt;span class="o"&gt;==&lt;/span&gt; Display every 100 iterations
display: 100
&lt;span class="o"&gt;==&lt;/span&gt; The maximum number of iterations
max_iter: 10000
&lt;span class="o"&gt;==&lt;/span&gt; snapshot intermediate results
snapshot: 5000
snapshot_prefix: &lt;span class="s2"&gt;"examples/mnist/lenet"&lt;/span&gt;
&lt;span class="o"&gt;==&lt;/span&gt; solver mode: CPU or GPU
solver_mode: CPU
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Finally, run this example:
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root@OrangePi:~/caffe# time sh examples/mnist/train_lenet.sh
I0110 06:28:06.117972 25078 caffe.cpp:197] Use CPU.
I0110 06:28:06.118988 25078 solver.cpp:45] Initializing solver from parameters:
test_iter: 100
test_interval: 500
base_lr: 0.01
display: 100
XUNLONG Software Co,. Ltd All rights reserved
36 www.orangepi.com
max_iter: 10000
lr_policy: "inv"
gamma: 0.0001
power: 0.75
momentum: 0.9
weight_decay: 0.0005
snapshot: 5000
snapshot_prefix: "examples/mnist/lenet"
solver_mode: CPU
net: "examples/mnist/lenet_train_test.prototxt"
......
......
......
I0110 07:25:37.016746 25078 sgd_solver.cpp:284] Snapshotting solver state to binary proto file
examples/mnist/lenet_iter_10000.solverstate
I0110 07:25:37.146054 25078 solver.cpp:327] Iteration 10000, loss = 0.00200602
I0110 07:25:37.146195 25078 solver.cpp:347] Iteration 10000, Testing net (#0)
I0110 07:25:54.299634 25080 data_layer.cpp:73] Restarting data prefetching from start.
I0110 07:25:55.010598 25078 solver.cpp:414] Test net output #0: accuracy = 0.9913
I0110 07:25:55.010757 25078 solver.cpp:414] Test net output #1: loss = 0.0273203 (* 1 =
0.0273203 loss)
I0110 07:25:55.010777 25078 solver.cpp:332] Optimization Done.
I0110 07:25:55.010793 25078 caffe.cpp:250] Optimization Done.
real 57m48.984s
user 58m6.479s
sys 0m1.977s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;⑦Operation conclusion According to the above results, the running time of the CPU is about 58 minutes, with an accuracy of about 99%.&lt;/p&gt;

</description>
      <category>4b</category>
      <category>armbian</category>
      <category>machinelearning</category>
      <category>merrykombo</category>
    </item>
    <item>
      <title>🍊 ARM your continuous integration system with fruits! 🍌</title>
      <dc:creator>Bruno Verachten</dc:creator>
      <pubDate>Wed, 22 Apr 2026 07:44:14 +0000</pubDate>
      <link>https://dev.to/gounthar/arm-your-continuous-integration-system-with-fruits-2ocj</link>
      <guid>https://dev.to/gounthar/arm-your-continuous-integration-system-with-fruits-2ocj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on January 11, 2021 at &lt;a href="https://bruno.verachten.fr/2021/01/11/arm-your-continuous-integration-system-with-fruits/" rel="noopener noreferrer"&gt;https://bruno.verachten.fr/2021/01/11/arm-your-continuous-integration-system-with-fruits/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The goal of this howto is to show you how to add your own ARM Gitlab runner to your Gitlab instance Runners herd.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F261%2F1%2AXetah-hQRnbxV0rX_qLkuQ.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F261%2F1%2AXetah-hQRnbxV0rX_qLkuQ.gif" alt="Image for post" width="261" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whaaaaaaaaaat?&lt;/p&gt;

&lt;p&gt;Ok, let me introduce quickly the concepts.&lt;/p&gt;

&lt;p&gt;Continuous integration allows the developers on a project to check that each change they made did not introduce any regression. Each commit will be followed by a build of their application, and by a run of their test suite. If every test runs still the same way after the commit, then all is fine. &lt;a href="https://gitlab.com" rel="noopener noreferrer"&gt;Gitlab&lt;/a&gt; is a web-based Git repository manager (with tons of features). You can use it directly online, install your own &lt;a href="https://gitlab.com/gitlab-org/gitlab-ce" rel="noopener noreferrer"&gt;community-edition&lt;/a&gt; copy, or &lt;a href="https://about.gitlab.com/pricing/" rel="noopener noreferrer"&gt;pay&lt;/a&gt; for top level support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DXLBMbQG4u-Q" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DXLBMbQG4u-Q" alt="watch?v=XLBMbQG4u Q" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Today, we’ll be only interested in their &lt;em&gt;continuous integration&lt;/em&gt; feature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F212%2F1%2ASa9nkYnFIuerM4xcVoYnJQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F212%2F1%2ASa9nkYnFIuerM4xcVoYnJQ.png" alt="Gitlab CI/CD logo" width="212" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Starting Continuous Integration (i.e. &lt;em&gt;CI&lt;/em&gt;) is really &lt;a href="https://docs.gitlab.com/ee/ci/quick_start/" rel="noopener noreferrer"&gt;straightforward&lt;/a&gt; with Gitlab. Just create a &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file in your repository hosted in Gitlab, populate it with the commands needed to build your application, and you’re good to go! 🎉 But wait… Where does that build and test execute? That’s where the runners enter. Runners are machines linked to Gitlab that will execute the builds. These can be real machines, virtual machines or containers. By default, you have access to shared runners on &lt;a href="http://gitlab.com" rel="noopener noreferrer"&gt;gitlab.com&lt;/a&gt; , or even on your company Gitlab instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiVWRp4ezNqY" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiVWRp4ezNqY" alt="watch?v=iVWRp4ezNqY" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These runners are tagged (🏷️) so that you can also tag your builds accordingly. If you need a runner that will build a Docker image, it could be tagged &lt;code&gt;docker&lt;/code&gt; for example, and so would be your build step in your &lt;code&gt;.gitlab.yml\&lt;/code&gt;file. Your builds can now easily be customized without ruining your own machine configuration… and even use Docker.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why would you add a 🏃?
&lt;/h1&gt;

&lt;p&gt;If you are using the main Gitlab shared instance, chances are that you will have to be patient to get the results of your build. The Shared Runner concept is really cool, as it allows to share the load, but … you’re not alone waiting in that queue… And the same could be true for your company’s Gitlab instance, as it will soon become popular. So you could add your own machine to the Gitlab Runners herd, just for your project. Pretty neat if you’re tired to wait for other people builds to finish. The problem is a new machine is not cheap. That’s way cool to have your own Gitlab Runner running in your office, but have a quick look at your pockets contents. Do you have enough cash to buy a new machine? Maybe not… But wait… In the introduction, I was talking about ARM Runners. Why would you need to add an ARM processor machine as a Gitlab Runner?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F500%2F1%2AO6QPb4jk0UsH6D2irsK5_w.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F500%2F1%2AO6QPb4jk0UsH6D2irsK5_w.gif" alt="Image for post" width="500" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reason why I chose to install an ARM Gitlab Runner&lt;/p&gt;

&lt;p&gt;Well, if your code does not depend on a specific family of processors, there is no other cheaper alternative to build a new Gitlab Runner.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;\$15&lt;/em&gt; for an OrangePi &lt;a href="https://fr.aliexpress.com/store/product/New-Orange-Pi-Zero-H2-Quad-Core-Open-source-512MB-development-board-beyond-Raspberry-Pi/1553371_32761500374.html?spm=a2g0w.12010612.8148356.3.6ab5799dZxeXWO" rel="noopener noreferrer"&gt;Zero&lt;/a&gt; machine able to run Docker and Gitlab Runner can’t be beaten. And… if you want to develop for that kind of architecture (ARMV7, aarch64…), you will need that kind of runner. I am trying to port software to the ARM processor, and these ARM Gitlab Runners have proven to be really useful. I can develop on my favorite IDE (on a X86 machine for example), commit, and automagically, my modified code will be compiled and tested on an ARM machine. I have to admit all of this could also be done with X86 Runners, as &lt;a href="https://www.qemu.org/" rel="noopener noreferrer"&gt;QEMU&lt;/a&gt; is able to emulate ARM processors. And even the build of ARM Docker image can be done on X86 Gitlab Runners, thanks to the amazing work of guys at &lt;a href="https://www.balena.io/" rel="noopener noreferrer"&gt;Balena&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F267%2F1%2AXnLd2UQBFJIlThSzr1WZmA.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F267%2F1%2AXnLd2UQBFJIlThSzr1WZmA.gif" alt="Image for post" width="267" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But where’s the fun? And furthermore… We all have a Raspberry Pi taking layer after layer of dust. Who has not been bored after having his first led blink? Why not put it back to work?&lt;/p&gt;

&lt;p&gt;Let’s go then for a cheap and fun solution.&lt;/p&gt;

&lt;h1&gt;
  
  
  Choose your preferred fruit
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F800%2F1%2AtMFaZclJuz1aZ1ykV2ezVA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F800%2F1%2AtMFaZclJuz1aZ1ykV2ezVA.png" alt="🍊 Pi Zero" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we’re not going to install Gitlab Runner on a 🍊 Pi, but on a &lt;a href="https://twitter.com/Poddingue/status/1017689162428895232" rel="noopener noreferrer"&gt;Raspberry Pi&lt;/a&gt; (because we had one laying around), but this should work on any &lt;a href="https://www.armbian.com/download/?specifications=docker" rel="noopener noreferrer"&gt;fruit&lt;/a&gt; (aka ARM Docker capable device), as &lt;a href="http://www.banana-pi.org/" rel="noopener noreferrer"&gt;🍌&lt;/a&gt; or &lt;a href="https://twitter.com/Poddingue/status/1022543760872366080" rel="noopener noreferrer"&gt;🍊&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F300%2F1%2AhH8ulrNvec1AE44yZxVj9Q.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F300%2F1%2AhH8ulrNvec1AE44yZxVj9Q.jpeg" alt="Raspberry Pi found on Amazon" width="300" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;image::&lt;a href="http://www.banana-pi.org/%5C%5B%5C%5B%F0%9F%8D%8C%5C" rel="noopener noreferrer"&gt;http://www.banana-pi.org/\[\[🍌\&lt;/a&gt;] Pi M1, courtesy of &lt;a href="https://www.armbian.com/bananapi/" rel="noopener noreferrer"&gt;Armbian&lt;/a&gt;](../assets/images/2021/02/04/bananapi.png)&lt;/p&gt;

&lt;p&gt;This version of Gitlab Runner we’ll use needs Docker, that’s why we need a Linux distribution which supports Docker. We’re using an &lt;a href="https://blog.hypriot.com/" rel="noopener noreferrer"&gt;Hypriot&lt;/a&gt; distro because of the Raspberry Pi. We could have used &lt;a href="https://www.raspberrypi.org/downloads/raspbian/" rel="noopener noreferrer"&gt;Raspbian&lt;/a&gt;, but HypriotOS is the best Docker-oriented distribution for the Raspberry Pi. From downloading the image to the first &lt;code&gt;docker \&lt;/code&gt;command will take only five minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F300%2F1%2A-9x8mY52ZJxiz77n6x6Chg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F300%2F1%2A-9x8mY52ZJxiz77n6x6Chg.gif" alt="From downloading to running Docker in 5 minutes" width="300" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also a 64bits fork (as lots of Raspberry Pi distributions are not available for 64bits, that’s a nice plus). To me, that’s the first choice if you want to use Docker on a Raspberry Pi out of the box.&lt;/p&gt;

&lt;p&gt;For all other devices, prefer &lt;a href="https://www.armbian.com/download/" rel="noopener noreferrer"&gt;Armbian&lt;/a&gt; if supported (like the Orange Pi Zero that can run as a Gitlab Runner too). It’s really easy to install &lt;a href="https://docs.armbian.com/User-Guide_Advanced-Features/#how-to-run-docker" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; on this distribution.&lt;/p&gt;

&lt;p&gt;First of all, let’s download the latest Hypriot distro.&lt;/p&gt;

&lt;h1&gt;
  
  
  Downloading and installing the Hypriot Distro
&lt;/h1&gt;

&lt;p&gt;Go to the Hypriot &lt;a href="https://blog.hypriot.com/downloads/" rel="noopener noreferrer"&gt;download&lt;/a&gt; page and choose the latest distro.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current image available on 02/04/2021 is Version 1.12.3 : &lt;a href="https://github.com/hypriot/image-builder-rpi/releases/download/v1.12.3/hypriotos-rpi-v1.12.3.img.zip" rel="noopener noreferrer"&gt;https://github.com/hypriot/image-builder-rpi/releases/download/v1.12.3/hypriotos-rpi-v1.12.3.img.zip&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  On Windows
&lt;/h1&gt;

&lt;p&gt;Next go to your &lt;em&gt;Download&lt;/em&gt; folder and extract the downloaded zip file by right-clicking on the file and then clicking on &lt;em&gt;7-zip → extract here&lt;/em&gt;…. if you use 7-zip. Whatever your unzipping tool may be, right-click and choose the right option to uncompress the file. After 7-zip is finished with extracting you will have a file with a &lt;code&gt;.img&lt;/code&gt; extension.&lt;/p&gt;

&lt;h1&gt;
  
  
  On OsX
&lt;/h1&gt;

&lt;p&gt;It’s almost the same as for Windows, except that the tool could be &lt;a href="https://www.keka.io/en/" rel="noopener noreferrer"&gt;Keka&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  On Linux
&lt;/h1&gt;

&lt;p&gt;Just use 7z.&lt;/p&gt;

&lt;h1&gt;
  
  
  Writing the image
&lt;/h1&gt;

&lt;p&gt;In the next step we will flash this file onto your SD card. We have to use the finest piece of sofware able to write &lt;em&gt;CORRECTLY&lt;/em&gt; and &lt;em&gt;CHECK&lt;/em&gt; the written image (i.e. &lt;a href="https://etcher.io/" rel="noopener noreferrer"&gt;Etcher&lt;/a&gt;) on any platform. Current version is 1.4.4.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Windows: &lt;a href="https://github.com/balena-io/etcher/releases/download/v1.5.116/balenaEtcher-Setup-1.5.116.exe?d_id=175dc868ae93-02925a057a102f-1a327340-1fa400-175dc868aea1d0" rel="noopener noreferrer"&gt;https://github.com/balena-io/etcher/releases/download/v1.5.116/balenaEtcher-Setup-1.5.116.exe?d_id=175dc868ae93-02925a057a102f-1a327340-1fa400-175dc868aea1d0&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;osX: &lt;a href="https://github.com/balena-io/etcher/releases/download/v1.5.116/balenaEtcher-1.5.116.dmg?d_id=175dc868ae93-02925a057a102f-1a327340-1fa400-175dc868aea1d0" rel="noopener noreferrer"&gt;https://github.com/balena-io/etcher/releases/download/v1.5.116/balenaEtcher-1.5.116.dmg?d_id=175dc868ae93-02925a057a102f-1a327340-1fa400-175dc868aea1d0&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Linux: &lt;a href="https://github.com/balena-io/etcher/releases/download/v1.5.116/balena-etcher-electron-1.5.116-linux-x64.zip?d_id=175dc868ae93-02925a057a102f-1a327340-1fa400-175dc868aea1d0" rel="noopener noreferrer"&gt;https://github.com/balena-io/etcher/releases/download/v1.5.116/balena-etcher-electron-1.5.116-linux-x64.zip?d_id=175dc868ae93-02925a057a102f-1a327340-1fa400-175dc868aea1d0&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Too bad it does not exist for armv7. I do insist on the use of Etcher, because lots of supposed Linux bugs are because of a badly written image, or to a faulty SDCard. Etcher will take care of these issues. If possible, do not use any USB Dongle or gizmo to host the SDCard, prefer your laptop own SDCard reader. And for the SDCard, prefer a &lt;em&gt;REAL&lt;/em&gt; and &lt;em&gt;GENUINE&lt;/em&gt; SDCard.&lt;/p&gt;

&lt;p&gt;How can you differentiate a real one from a bad copy? Well, you can’t, or not easily… Anyway, unzip the image, and write it with Etcher to your SDCard. In the latests versions of Etcher, you don’t even need to unzip the image file beforehand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F800%2F1%2AqLX-092qNGY-JcfHQIGQig.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F800%2F1%2AqLX-092qNGY-JcfHQIGQig.gif" alt="Etcher process" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upon completion, close Etcher and eject the SD card before removing it from its slot. In order to eject the SD card, use the secure eject function in the menu next to your system clock. This is usually in the very bottom right corner of your screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F439%2F1%2ANl9P9s7CchJC1-Vu-F2zIw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F439%2F1%2ANl9P9s7CchJC1-Vu-F2zIw.png" alt="Eject SDCard" width="439" height="133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now it’s time to start your Raspberry Pi with the SD card&lt;/p&gt;

&lt;h1&gt;
  
  
  Boot your Raspberry Pi
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Connect the Raspberry Pi to your local network via an Ethernet cable&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;put the SD card into the designated slot&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;plug in the power adapter&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After finishing the last step your Raspberry Pi will boot and the LEDs should start blinking. The very first boot will take one to three minutes as the file system will be resized.&lt;/p&gt;

&lt;h1&gt;
  
  
  Find its IP on the network
&lt;/h1&gt;

&lt;p&gt;After booting, you can find the Raspberry Pi at your network with a simple &lt;code&gt;ping black-pearl.local \&lt;/code&gt;— no more searching for IP addresses required thanks to the integrated Avahi service discovery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1802%2F1%2AmghqK-yJmbIRv0ajDYfdYw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1802%2F1%2AmghqK-yJmbIRv0ajDYfdYw.png" alt="Image for post" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/754f2988dc9ee643ea1d8be9a9c42223/raw/5437d39cdb07ab6bfb536ae468b483d8c197a29a/ssh%20pirate.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/754f2988dc9ee643ea1d8be9a9c42223/raw/5437d39cdb07ab6bfb536ae468b483d8c197a29a/ssh%2520pirate.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see in the following &lt;code&gt;ifconfig&lt;/code&gt; command, the MAC Address of the Raspberry always starts with &lt;code&gt;B8&lt;/code&gt;. That could prove useful if ever this first method does not work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1296%2F1%2AsC_cfNa1I4Y_NIjJZSQuaw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1296%2F1%2AsC_cfNa1I4Y_NIjJZSQuaw.png" alt="Image for post" width="800" height="901"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/ceb2680f38e613289f4e55b9e8c4f819/raw/0e7888a483783a92eba88f5d1f9996a47b54cea3/ifconfig.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/ceb2680f38e613289f4e55b9e8c4f819/raw/0e7888a483783a92eba88f5d1f9996a47b54cea3/ifconfig.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, what shall we do if the very first &lt;code&gt;ping/ssh `does not work with `black-pearl&lt;/code&gt;? Well, as the Raspberry Pis MAC Address always start with &lt;code&gt;B8&lt;/code&gt; we can use nmap and keep only the IPs whose attached MAC Address sport a &lt;code&gt;B8&lt;/code&gt;(yes, it’s gross). If your network starts at &lt;code&gt;10.XX.30.XX `and ends at `10.XX.50.XX&lt;/code&gt;, &lt;code&gt;ip_start&lt;/code&gt; would be 30 and &lt;code&gt;ip_end&lt;/code&gt; would be 50.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1954%2F1%2Asr4MYJR2SaQesLg8r5WXrw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1954%2F1%2Asr4MYJR2SaQesLg8r5WXrw.png" alt="Image for post" width="800" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/2b4372abd7a623f7cf670575a76026d1/raw/e27c56e792fdf73652b4b53d07fa6bb91df1b7c7/nmap.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/2b4372abd7a623f7cf670575a76026d1/raw/e27c56e792fdf73652b4b53d07fa6bb91df1b7c7/nmap.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will give you a list of Raspberry IPs on which you will then be able to try to connect with&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1498%2F1%2AxMIFpYi-0JmG-NoyElhFXA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1498%2F1%2AxMIFpYi-0JmG-NoyElhFXA.png" alt="Image for post" width="800" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Machine name and user password
&lt;/h1&gt;

&lt;p&gt;The default user “pirate” (password “hypriot”) can be changed or removed before the first boot. Everything happens in &lt;code&gt;/boot/user-data&lt;/code&gt;. You can add your public SSH key, disable password logins and specify a different user account before booting your Raspberry Pi. WiFi can be customized and enabled to have Docker up and running through the air without attaching a keyboard and monitor. How cool is that? You can also change the name of the machine later on with &lt;code&gt;sudo raspi-config&lt;/code&gt;. The changes will have to be made in &lt;code&gt;Network Options/hostname&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F849%2F1%2Aj0rkCaEBkDxCzlsTjruMhQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F849%2F1%2Aj0rkCaEBkDxCzlsTjruMhQ.png" alt="Image for post" width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once it’s done, reboot and enjoy your new machine name:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1734%2F1%2AanyIpGY7IcZQaxMc7MCOtA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1734%2F1%2AanyIpGY7IcZQaxMc7MCOtA.png" alt="Image for post" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/596bc88f23d98de76bb70b11eb66e47b/raw/13f2210c233b6e1dc4d8da31f72e9063cfd18271/ssh%20pirate%20arm%20runner.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/596bc88f23d98de76bb70b11eb66e47b/raw/13f2210c233b6e1dc4d8da31f72e9063cfd18271/ssh%2520pirate%2520arm%2520runner.txt&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Proxy settings
&lt;/h1&gt;

&lt;p&gt;If you’re like me, stuck behind a corporate proxy, you will have to set a few things before being able to update your machine and run Docker.&lt;/p&gt;

&lt;h1&gt;
  
  
  Package manager
&lt;/h1&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/apt/apt.conf.d/10proxy&lt;/code&gt; and add your proxy (create it if it doesn’t exist yet):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1176%2F1%2AYO2yRq5sowAncx9kg65JZQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1176%2F1%2AYO2yRq5sowAncx9kg65JZQ.png" alt="Image for post" width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/e3288a613845b18aee3fe1098422d59f/raw/2eb3c68c8f2b3473902763e2f2b22be79918eea6/acquire.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/e3288a613845b18aee3fe1098422d59f/raw/2eb3c68c8f2b3473902763e2f2b22be79918eea6/acquire.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;sudo apt-get update&lt;/code&gt; should prove that the configuration is working&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1666%2F1%2AAlHcN0pshfekudRahrvB8A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1666%2F1%2AAlHcN0pshfekudRahrvB8A.png" alt="Image for post" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/dc7829b564944c460e16a68870c13409/raw/afb35d6fe8731476befdf5f62f5513d4372f3f6a/apt%20update.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/dc7829b564944c460e16a68870c13409/raw/afb35d6fe8731476befdf5f62f5513d4372f3f6a/apt%2520update.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While you’re there, update the machine with &lt;code&gt;sudo apt-get upgrade&lt;/code&gt; to have the freshest Docker environment available.&lt;/p&gt;

&lt;h1&gt;
  
  
  Docker
&lt;/h1&gt;

&lt;p&gt;If you try to download a Docker image with &lt;code&gt;docker pull&lt;/code&gt; … it will fail because Docker doesn’t use the same proxy settings as &lt;code&gt;apt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2Ahkun6URrs0SUNF8JEhKOgg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2Ahkun6URrs0SUNF8JEhKOgg.png" alt="Image for post" width="800" height="190"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/8384bb56d7ef8a9380c1f0d43013162e/raw/4f1f2aa8837966fe040e88c9e5c1aefc48d9f17a/docker%20pull.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/8384bb56d7ef8a9380c1f0d43013162e/raw/4f1f2aa8837966fe040e88c9e5c1aefc48d9f17a/docker%2520pull.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So you will have to create a few directories and files to let Docker know it should use your proxy:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1278%2F1%2AK3JeXg1j_LuDgHJFqdALRA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1278%2F1%2AK3JeXg1j_LuDgHJFqdALRA.png" alt="Image for post" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/67dfa1f13e3d3064a02d17cb3ed9a51d/raw/3bb338cd496e0032f2472178f23fe96416eee0a2/docker%20http%20proxy.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/67dfa1f13e3d3064a02d17cb3ed9a51d/raw/3bb338cd496e0032f2472178f23fe96416eee0a2/docker%2520http%2520proxy.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/etc/systemd/system/docker.service.d/https-proxy.conf&lt;/code&gt; will contain:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2004%2F1%2Abtk9NYt7I0kj2RerYIfrNA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2004%2F1%2Abtk9NYt7I0kj2RerYIfrNA.png" alt="Image for post" width="799" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/d04bb35e54b87f1a97e7ef6493f746a3/raw/2af846f014e46c76df96f22f0cbdcbe70fd35926/proxy%20conf.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/d04bb35e54b87f1a97e7ef6493f746a3/raw/2af846f014e46c76df96f22f0cbdcbe70fd35926/proxy%2520conf.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;http `version of this file will have the same proxy settings (just remove the `S&lt;/code&gt; of &lt;code&gt;HTTPS&lt;/code&gt;). Once you’re done with the modifications, you will have to tell Docker to restart to take this into account:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F740%2F1%2Ab9iqNBZqH3bh1CXNUKrIGA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F740%2F1%2Ab9iqNBZqH3bh1CXNUKrIGA.png" alt="Image for post" width="740" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/107de82adde721e3f7803778b3e08ffb/raw/c0d8670a60efe2585880aaf07a11e398ee17d230/restart%20docker" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/107de82adde721e3f7803778b3e08ffb/raw/c0d8670a60efe2585880aaf07a11e398ee17d230/restart%2520docker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then you’ll be able to check that your modifications are correct:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2A5Aw9XONC5xCoQNmyPLPmDQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2A5Aw9XONC5xCoQNmyPLPmDQ.png" alt="Image for post" width="800" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/2b1df86210ee515b1c074e7140d29b35/raw/bad834d6d2c23387ed8c5097fec0a52d29805056/properties.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/2b1df86210ee515b1c074e7140d29b35/raw/bad834d6d2c23387ed8c5097fec0a52d29805056/properties.txt&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Date
&lt;/h1&gt;

&lt;p&gt;Check that the date on your machine is correct by issuing date in the terminal. If it’s not correct, you will have some troubles with getting a Docker image. So, if the date/time are not right, you can set them up with:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1116%2F1%2Ai5I2L1DMfeQzFosP6xE81Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1116%2F1%2Ai5I2L1DMfeQzFosP6xE81Q.png" alt="Image for post" width="800" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/ecc647cb01fe16bcd2f439d492b1e663/raw/15c97e75b7f8164a92f19dfb834fc9a6381fea9c/date.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/ecc647cb01fe16bcd2f439d492b1e663/raw/15c97e75b7f8164a92f19dfb834fc9a6381fea9c/date.txt&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Gitlab Runner
&lt;/h1&gt;

&lt;p&gt;You already know Gitlab Runner, I’m sure you are already using some shared runners with your CI on your company’s gitlab. Most of the time, you don’t need to setup your own, but as there aren’t that many ARM gitlab runners for the time being… &lt;a href="https://forum.gitlab.com/t/updated-the-docker-image-of-gitlab-runner-for-arm/7477" rel="noopener noreferrer"&gt;Gitlab Runner&lt;/a&gt; has been ported to ARM in 2017 by &lt;a href="https://gitlab.com/ulm0" rel="noopener noreferrer"&gt;Mauricio Ugaz&lt;/a&gt; as it wasn’t available, and everyone needs gitlab-runner on Docker running on ARM, am I right?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: there are two versions available. Alpine and Ubuntu. As Alpine is the default image, when you run docker pull klud/gitlab-runner Alpine version will be downloaded.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  About the image
&lt;/h1&gt;

&lt;p&gt;This image is built for ARM devices, based on the official repo of the GitLab Runner. That’s a fantastic work that made my daily work really more enjoyable.&lt;/p&gt;

&lt;h1&gt;
  
  
  Overview
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Runner container setup&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You need to mount a volume into the gitlab-runner container in order to share the configuration. This could be done this way:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1548%2F1%2A5H7Ch6DPY0i3yvu5ZKscLQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1548%2F1%2A5H7Ch6DPY0i3yvu5ZKscLQ.png" alt="Image for post" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/925bd7f20136b68b7a1bf337b31ff9ac/raw/4a48f4353261d247e0696ef581cd829bc44242ad/docker%20run%20txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/925bd7f20136b68b7a1bf337b31ff9ac/raw/4a48f4353261d247e0696ef581cd829bc44242ad/docker%2520run%2520txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we want to use the Docker executor, it is necessary to mount the Docker socket this way. Don’t forget to create &lt;code&gt;/etc/gitlab-runner \&lt;/code&gt;directory ahead of time:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1296%2F1%2AtBEuZu_v5u2MTQT9kH8Zgg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1296%2F1%2AtBEuZu_v5u2MTQT9kH8Zgg.png" alt="Image for post" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/9c76e1523d04a29d490703fe68c32793/raw/79db3c2dd7f67d1083c77a2ac1109fc6d9b53e50/docker%20run%20II.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/9c76e1523d04a29d490703fe68c32793/raw/79db3c2dd7f67d1083c77a2ac1109fc6d9b53e50/docker%2520run%2520II.txt&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Register runner
&lt;/h1&gt;

&lt;p&gt;Once the container is up and running, you can then register the runner on your GitLab server. Go to your project on your Gitlab instance, click on &lt;em&gt;Settings&lt;/em&gt;, then on &lt;em&gt;CI/CD&lt;/em&gt;, and click on &lt;em&gt;Expand&lt;/em&gt; next to &lt;em&gt;Runner settings&lt;/em&gt;. Scroll down to &lt;em&gt;Setup a specific Runner manually&lt;/em&gt;, and note the information regarding the &lt;em&gt;URL&lt;/em&gt; and the &lt;em&gt;token&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiVWRp4ezNqY" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiVWRp4ezNqY" alt="watch?v=iVWRp4ezNqY" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specify the following URL during the Runner setup: &lt;a href="https://gitlab.com/" rel="noopener noreferrer"&gt;https://gitlab.com/&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the following registration token during setup: &lt;code&gt;the_token_that_is_yours_and_yours_alone&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1244%2F1%2AgDGS8S2SxTGEqh5bL5v7pw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1244%2F1%2AgDGS8S2SxTGEqh5bL5v7pw.gif" alt="Image for post" width="600" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So once you have the information you need (URL and token), you can then do this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2AqdEiNG1XrIWXcUSuJyJZKQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2AqdEiNG1XrIWXcUSuJyJZKQ.png" alt="Image for post" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/968c840f9a5ef8ee98edfbd6d6154fff/raw/3ea52f940b92282d7fa97ebc5e8ab0ff3fb6219e/register.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/968c840f9a5ef8ee98edfbd6d6154fff/raw/3ea52f940b92282d7fa97ebc5e8ab0ff3fb6219e/register.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, with the latest versions of Docker from 19.03, you may get this message.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fdocker%2520error.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fdocker%2520error.png" alt="docker error" width="798" height="98"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With Docker 19.03, TLS is enabled by default, to use TLS you need to update the GitLab Runner configuration so that the certificates are shared between the service and build container. To do this, update your config.toml so that it looks like that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fconfig%2520toml%2520modification%2520docker%252019.03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fconfig%2520toml%2520modification%2520docker%252019.03.png" alt="config toml modification docker 19.03" width="799" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s check if this new runner can now be seen. Refresh your CI/CI settings, expand &lt;em&gt;Runner settings&lt;/em&gt;, and go down. You should now be able to see your brand new Gitlab-ci runner proudly sporting its ARM tag! 🤩&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DZF7_Dwp7ISc" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DZF7_Dwp7ISc" alt="watch?v=ZF7 Dwp7ISc" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;!--&lt;a href="https://miro.medium.com/max/1244/1*ahzX2cz-Pnr51VyBcKZ0VA.gif" rel="noopener noreferrer"&gt;Image for post&lt;/a&gt;-→&lt;/p&gt;

&lt;p&gt;Congrats!&lt;/p&gt;

&lt;p&gt;You could also do it this way:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1278%2F1%2ApdAfyHtHj_9FgmpEBbH6tA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1278%2F1%2ApdAfyHtHj_9FgmpEBbH6tA.png" alt="Image for post" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/d6ce6108e3f25a11a8c70bc74ab65e97/raw/aaa4af34343124e9d3dbed731cffd1dde6c33243/register%20II.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/d6ce6108e3f25a11a8c70bc74ab65e97/raw/aaa4af34343124e9d3dbed731cffd1dde6c33243/register%2520II.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So now you have your own ARM runner, ready to work.&lt;/p&gt;

&lt;h1&gt;
  
  
  Tip
&lt;/h1&gt;

&lt;p&gt;For my case, I am using the standard image to build my Docker images with this Runner, and it works just fine. Dockerfiles and info about Docker in Docker images for ARM are &lt;a href="https://gitlab.com/ulm0/docker-arm" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Troubleshooting
&lt;/h1&gt;

&lt;p&gt;In case you’re using docker in docker in the runner, you may experience some issues&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2ADgX3iSEvrsmVt3Ec9ewAEA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2ADgX3iSEvrsmVt3Ec9ewAEA.png" alt="Image for post" width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/7fd4ce41ebdd8b79bf7dfe2472af7119/raw/a3d0b0ff8891609078695ea60de0fc69e7d76fa0/daemon.txt" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/7fd4ce41ebdd8b79bf7dfe2472af7119/raw/a3d0b0ff8891609078695ea60de0fc69e7d76fa0/daemon.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So in order to address this issue you need to look within the config folder you mounted in the runner container. There is a config file which is&lt;code&gt;/etc/gitlab-runner/config.toml\&lt;/code&gt;. You need to add two lines (regarding the &lt;code&gt;privileged`and the `volumes)`and then restart the runner with `docker restart arm-runner&lt;/code&gt;. If you can’t find the file, it is maybe in &lt;code&gt;~/.runner/config.toml/&lt;/code&gt;; copy it in &lt;code&gt;/etc/gitlab-runner&lt;/code&gt; after editing it and restart docker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1204%2F1%2A1n0krvtUogmuodKiOQRQaw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F1204%2F1%2A1n0krvtUogmuodKiOQRQaw.png" alt="Image for post" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.githubusercontent.com/gounthar/31d41be5afb4b2c3ce0c66dfc1f00f33/raw/b08565f575646763ca3f71321a625e47f3c3619b/config.toml" rel="noopener noreferrer"&gt;https://gist.githubusercontent.com/gounthar/31d41be5afb4b2c3ce0c66dfc1f00f33/raw/b08565f575646763ca3f71321a625e47f3c3619b/config.toml&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So now you can enjoy your first ARM Gitlab build!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2AFKOoVagyEh6SGb63ogWALA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fmax%2F2048%2F1%2AFKOoVagyEh6SGb63ogWALA.png" alt="Image for post" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We now have an ARM machine sporting a recent kernel, running a recent version of gitlab-runner on top of a recent version of Docker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fgitlab%2520runner%2520arm%2520version.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fgitlab%2520runner%2520arm%2520version.png" alt="gitlab runner arm version" width="394" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fhypriot%2520docker%2520version.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fhypriot%2520docker%2520version.png" alt="hypriot docker version" width="432" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fhypriot%2520screenfetch.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fhypriot%2520screenfetch.png" alt="hypriot screenfetch" width="535" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have reached the first level of achievement, having Gitlab runner running on the Pi thanks to Docker. Can we go any further? Could we build Docker images thanks to Gitlab runner on the Pi?&lt;/p&gt;

&lt;h1&gt;
  
  
  Building Docker Images
&lt;/h1&gt;

&lt;p&gt;If you’re going to build images on this runner you can use the standard Docker image, as it is multi-arch now. Just type docker at “Please enter the Docker image” in the first method or in &lt;code&gt;--docker- image "image:tag"&lt;/code&gt; with the second method. Sometimes, you may experience some troubles when using Docker in Docker. So in order to address this issue you need to look within the config folder you mounted in the runner container. There is a config file which is &lt;code&gt;/etc/gitlab-runner/config.toml&lt;/code&gt;. You need to add two lines (regarding the privileged and the volumes)and then restart the runner with docker restart arm-runner.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fprivileged.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fprivileged.png" alt="privileged" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you can’t find the file, it is maybe in &lt;code&gt;~/.runner/config.toml/&lt;/code&gt; copy it in &lt;code&gt;/etc/gitlab-runner&lt;/code&gt; after editing it and restart docker.&lt;/p&gt;

&lt;p&gt;It looks like we’ve reached the second level of achievement, being able to build Docker images through Gitlab runner.&lt;/p&gt;

&lt;p&gt;What about another challenge?&lt;/p&gt;

&lt;h1&gt;
  
  
  Super tiny Runner
&lt;/h1&gt;

&lt;p&gt;Let’s try it on another SBC, much, much cheaper, with half the amount of memory. This time, we’ll change fruit, let’s leave the raspberry aside and try with an Orange… Do you think it could work?&lt;/p&gt;

&lt;p&gt;The Orange Pi Zero is one of my preferred SBCs, and almost the cheapest to this date.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Forangepizero.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Forangepizero.png" alt="orangepizero" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll get if for about 9€, and it can run Docker with a very recent kernel thanks to the Armbian distro. This distro is a fantastic community project which allows lots of SBCs to run recent Linux kernels with optimizations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Farmbian.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Farmbian.png" alt="armbian" width="565" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.armbian.com/" rel="noopener noreferrer"&gt;Armbian&lt;/a&gt; is a lightweight Debian based distribution specialized for ARM developing boards. It’s Compiled from scratch, It has powerful build and software development tools, and a vibrant community to boot. It supports lots of chips like the Allwinner H2+, H3, H5, Amlogic S905, Rockchip RK3288 and RK3399, Samsung Exynos 5422… If we go to the download section you can see there are tons of different cards that can run Armbian.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7JyZGcYTL00" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7JyZGcYTL00" alt="watch?v=7JyZGcYTL00" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- INSERT HERE DOWNLOAD SECTION VIDEO -→&lt;/p&gt;

&lt;p&gt;It you choose OrangePi zero, you can see the specifications. image::&lt;a href="https://www.youtube.com/watch?v=dX9I6xDn5AE%5C%5B%5C%5D" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=dX9I6xDn5AE\[\]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of them is Docker, which means you can run Docker on this board with Armbian.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdX9I6xDn5AE" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdX9I6xDn5AE" alt="watch?v=dX9I6xDn5AE" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- INSERT HERE ORANGE PI ZERO ARMBIAN VIDEO -→&lt;/p&gt;

&lt;p&gt;Yes, you can run Docker on a 512MB ARM machine. You can even run gitlab runner on Docker on it. And even Docker on gitlab runner on Docker on it… Il you click on Docker, you will then be able to see the list of SBCs able to run Docker with Armbian.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTnE1upS-QlQ" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTnE1upS-QlQ" alt="watch?v=TnE1upS QlQ" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- INSERT HERE DOCKER MACHINES ARMBIAN VIDEO -→&lt;/p&gt;

&lt;p&gt;Impressive, isn’t it?&lt;/p&gt;

&lt;p&gt;Docker does not come preinstalled with Armbian, so you have to install it like that.&lt;/p&gt;

&lt;p&gt;image::../assets/images/2021/02/04/docker install on armbian.png[docker install on armbian]Note that you could get Docker preinstalled, as you can build this distro by yourself as long as you have an Ubuntu machine available. The rest of the commands to run gitlab runner are strictly the same than for Raspberry Pi, so no need to repeat them. Let’s just have a look at the result, once the runner has been installed, registered and started. Feel like taking a look ?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fdocker%2520on%2520docker%2520runner.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fbruno.verachten.fr%2F..%2Fassets%2Fimages%2F2021%2F02%2F04%2Fdocker%2520on%2520docker%2520runner.gif" alt="docker on docker runner" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a build of a Docker image on an Orange Pi Zero. Isn’t it fantastic? We reached the third level of Achievement…&lt;/p&gt;

&lt;h1&gt;
  
  
  64 bits Runner
&lt;/h1&gt;

&lt;p&gt;Some of the boards handled by Armbian or HypriotOS are ARM64 also called aarch64, or ARMV8. To tell you the truth, even the Raspberry Pi 3B is aarch64… It’s just that Raspbian does not handle it for compatibility reasons with their older boards. You can find 64 bits distros for the Raspberry Pi, like &lt;a href="https://itsfoss.com/install-arch-raspberry-pi/" rel="noopener noreferrer"&gt;ArchLinux&lt;/a&gt;, &lt;a href="https://dietpi.com/docs/" rel="noopener noreferrer"&gt;DietPi&lt;/a&gt;, &lt;a href="https://blog.hypriot.com/downloads/" rel="noopener noreferrer"&gt;Hypriot&lt;/a&gt;… For other cards, Armbian does exist for &lt;a href="https://www.armbian.com/download/?specifications=64bit" rel="noopener noreferrer"&gt;ARM64&lt;/a&gt;, so does &lt;a href="https://docs.docker.com/engine/install/binaries/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;. Until 10 months ago, we were kind of stuck, as gitlab-runner for aarch64 did not exist image::&lt;a href="https://www.youtube.com/watch?v=dfP3wjcpveY%5C%5B%5C%5D" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=dfP3wjcpveY\[\]&lt;/a&gt; &amp;lt;!-- INSERT HERE MERGE REQUEST VIDEO -→&lt;/p&gt;

&lt;p&gt;The merge &lt;a href="https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/725" rel="noopener noreferrer"&gt;request&lt;/a&gt; had been running for almost 3 years until it got accepted. We are now able to use gitlab-runner on our 64 bits ARM machine, and without using Mauricio Ugaz &lt;a href="https://gitlab.com/ulm0/gitlab-runner" rel="noopener noreferrer"&gt;fork&lt;/a&gt;, just the standard Docker image directly supplied by gitlab.&lt;/p&gt;

&lt;p&gt;To sum up, we managed to setup a gitlab-ci Runner on a Raspberry Pi, and then on an OrangePi Zero . You now know how to transform your dusty Raspi into a gitlab- runner, even able to build Docker images. In the company I work for, we have also installed it on other SBCs (Odroid XU4, Nano Pi Fire3 ), and on a ThunderX2 just for fun. We are using them everyday, as they are cheap, disposable, fun to use, and have a very small footprint, so that we install them just about anywhere. I hope to have tickled your curiosity regarding these little beasts called SBCs, and that some of you will join the peaceful ARM rebellion.&lt;/p&gt;

</description>
      <category>armbian</category>
      <category>ci</category>
      <category>hypriot</category>
      <category>orangepi</category>
    </item>
  </channel>
</rss>
