Skip to content

Instantly share code, notes, and snippets.

@farhangnaderi
Created June 17, 2026 15:36
Show Gist options
  • Select an option

  • Save farhangnaderi/26c83c8aff86e5b1552f902fc245f917 to your computer and use it in GitHub Desktop.

Select an option

Save farhangnaderi/26c83c8aff86e5b1552f902fc245f917 to your computer and use it in GitHub Desktop.
A layered, composable mental model for the PX4 build system: target classes, runtime backends, and variants as independent axes.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PX4 Build Model — Conceptual Architecture</title>
<style>
:root {
--bg:#0d1117; --panel:#161b22; --border:#30363d; --text:#c9d1d9; --muted:#8b949e;
--head:#f0f6fc; --accent:#58a6ff; --green:#3fb950; --amber:#d29922; --red:#f85149; --purple:#bc8cff;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--text);
font:16px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; }
.wrap { max-width:920px; margin:0 auto; padding:0 28px 110px; }
header.hero { padding:56px 0 8px; }
header.hero .kicker { color:var(--accent); font-size:13px; letter-spacing:.12em; text-transform:uppercase; font-weight:600; }
header.hero h1 { font-size:34px; line-height:1.2; margin:10px 0 8px; color:var(--head); letter-spacing:-0.02em; }
header.hero .sub { color:var(--muted); font-size:16px; max-width:680px; }
h2 { color:var(--head); font-size:24px; margin:52px 0 6px; letter-spacing:-0.01em; }
h2 .n { color:var(--accent); font-variant-numeric:tabular-nums; margin-right:10px; opacity:.8; }
h3 { color:var(--head); font-size:18px; margin:30px 0 8px; }
p { margin:14px 0; }
.lead { font-size:18px; color:var(--text); }
strong { color:var(--head); font-weight:600; }
em { color:var(--purple); font-style:normal; }
code { font-family:var(--mono); font-size:13.5px; background:#1c2128; border:1px solid var(--border);
padding:1px 5px; border-radius:4px; color:#e6edf3; }
ul { margin:14px 0; padding-left:22px; } li { margin:8px 0; }
.fig { background:var(--panel); border:1px solid var(--border); border-radius:12px;
padding:24px 24px 14px; margin:26px 0; }
.fig svg { display:block; width:100%; height:auto; }
.fig .cap { color:var(--muted); font-size:13px; margin-top:16px; padding-top:13px; border-top:1px solid var(--border); }
.fig .cap b { color:var(--head); }
.fig .cap i { color:var(--text); font-style:italic; }
.callout { border-radius:12px; padding:16px 20px; margin:24px 0; border:1px solid var(--border); background:var(--panel); }
.callout.key { border-left:3px solid var(--accent); }
.callout.key b { color:var(--accent); }
.callout.dec { border-left:3px solid var(--amber); background:rgba(210,153,34,.06); }
.callout.dec b { color:var(--amber); }
table { border-collapse:collapse; width:100%; margin:20px 0; font-size:14.5px; }
th,td { border:1px solid var(--border); padding:10px 14px; text-align:left; vertical-align:top; }
th { background:var(--panel); color:var(--head); font-size:13px; text-transform:uppercase; letter-spacing:.05em; }
td b { color:var(--head); }
.footer { margin-top:60px; padding-top:18px; border-top:1px solid var(--border); color:var(--muted); font-size:13.5px; }
/* svg shared text styling */
svg text { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; }
svg .lbl { fill:var(--head); font-weight:600; }
svg .sub { fill:var(--muted); }
svg .mono { font-family:var(--mono); }
</style>
</head>
<body>
<div class="wrap">
<header class="hero">
<div class="kicker">PX4 Build System &middot; Conceptual Architecture</div>
<h1>What a PX4 firmware build actually is</h1>
<p class="sub">A mental model for how we describe, compose, and produce firmware &mdash; before we touch a line of CMake. This is the thing to argue about on a whiteboard. No file names, no migration plan, just the shape.</p>
</header>
<p class="lead">PX4's build system was designed around one assumption: <strong>one board produces one firmware, configured by one file.</strong> That assumption is no longer true, and everything that hurts today is a symptom of stretching it past its limit.</p>
<p>This document proposes a single replacement idea: stop describing builds as <em>files in a folder</em> and start describing them as <em>points in a space with independent axes</em>. Everything else follows from that.</p>
<!-- ===================== 1 ===================== -->
<h2><span class="n">01</span>The model we have today</h2>
<p>Today a build is named by a <strong>board folder</strong> and a single <strong>label</strong> &mdash; the name of one file in that folder. That label is the only knob. So the label has been forced to mean four completely different things at once:</p>
<div class="fig">
<svg viewBox="0 0 800 290" role="img" aria-label="One label forced to mean four different things">
<!-- top node -->
<rect x="330" y="14" width="140" height="40" rx="8" fill="#1c2128" stroke="#30363d"/>
<text x="400" y="39" text-anchor="middle" class="lbl mono" font-size="15">ONE LABEL</text>
<!-- fan-out lines -->
<g stroke="#30363d" stroke-width="1.5" fill="none">
<path d="M400 54 L400 74 L120 74 L120 96"/>
<path d="M400 74 L320 74 L320 96"/>
<path d="M400 74 L520 74 L520 96"/>
<path d="M400 54 L400 74 L700 74 L700 96"/>
</g>
<!-- four boxes -->
<g font-size="13">
<rect x="40" y="98" width="160" height="170" rx="8" fill="rgba(63,185,80,.07)" stroke="#3fb950"/>
<text x="120" y="120" text-anchor="middle" class="lbl" fill="#3fb950">vehicle type</text>
<text x="120" y="146" text-anchor="middle" class="sub mono">copter</text>
<text x="120" y="166" text-anchor="middle" class="sub mono">rover</text>
<text x="120" y="186" text-anchor="middle" class="sub mono">fixedwing</text>
<text x="120" y="206" text-anchor="middle" class="sub mono">uuv</text>
<text x="120" y="226" text-anchor="middle" class="sub mono">spacecraft</text>
<rect x="240" y="98" width="160" height="170" rx="8" fill="rgba(88,166,255,.07)" stroke="#58a6ff"/>
<text x="320" y="120" text-anchor="middle" class="lbl" fill="#58a6ff">build variant</text>
<text x="320" y="146" text-anchor="middle" class="sub mono">zenoh</text>
<text x="320" y="166" text-anchor="middle" class="sub mono">mavlink-dev</text>
<text x="320" y="186" text-anchor="middle" class="sub mono">performance</text>
<text x="320" y="206" text-anchor="middle" class="sub mono">test / debug</text>
<rect x="440" y="98" width="160" height="170" rx="8" fill="rgba(210,153,34,.07)" stroke="#d29922"/>
<text x="520" y="120" text-anchor="middle" class="lbl" fill="#d29922">bootloader</text>
<text x="520" y="146" text-anchor="middle" class="sub mono">bootloader</text>
<text x="520" y="166" text-anchor="middle" class="sub mono">canbootloader</text>
<text x="520" y="186" text-anchor="middle" class="sub mono">secureboot</text>
<rect x="640" y="98" width="160" height="170" rx="8" fill="rgba(188,140,255,.07)" stroke="#bc8cff"/>
<text x="720" y="120" text-anchor="middle" class="lbl" fill="#bc8cff">peripheral</text>
<text x="720" y="150" text-anchor="middle" class="sub">no label at all —</text>
<text x="720" y="170" text-anchor="middle" class="sub">a magic string</text>
<text x="720" y="190" text-anchor="middle" class="sub">hidden inside the</text>
<text x="720" y="210" text-anchor="middle" class="sub">board defaults</text>
</g>
</svg>
<div class="cap"><b>The overload.</b> Four unrelated questions &mdash; <i>what vehicle? what build flavor? is it a bootloader? is it a sensor node?</i> &mdash; all answered by one filename. "Peripheral" isn't even a label; it's a string buried in the board's defaults, which is why tools downstream have to <i>guess</i> a build's purpose by pattern-matching names.</div>
</div>
<p>And the model has no concept of a build producing more than one thing. Every board is assumed to be: <strong>one config &rarr; one compile &rarr; one firmware file.</strong></p>
<div class="fig">
<svg viewBox="0 0 800 90" role="img" aria-label="Linear one-to-one build pipeline">
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#8b949e"/>
</marker>
</defs>
<g font-size="13.5">
<rect x="20" y="24" width="150" height="42" rx="8" fill="#1c2128" stroke="#30363d"/>
<text x="95" y="50" text-anchor="middle" class="lbl">board folder</text>
<rect x="225" y="24" width="130" height="42" rx="8" fill="#1c2128" stroke="#30363d"/>
<text x="290" y="50" text-anchor="middle" class="lbl">one config</text>
<rect x="410" y="24" width="130" height="42" rx="8" fill="#1c2128" stroke="#30363d"/>
<text x="475" y="50" text-anchor="middle" class="lbl">one compile</text>
<rect x="595" y="24" width="150" height="42" rx="8" fill="#1c2128" stroke="#30363d"/>
<text x="670" y="50" text-anchor="middle" class="lbl">one firmware</text>
</g>
<g stroke="#8b949e" stroke-width="1.5" marker-end="url(#ar)">
<line x1="172" y1="45" x2="221" y2="45"/>
<line x1="357" y1="45" x2="406" y2="45"/>
<line x1="542" y1="45" x2="591" y2="45"/>
</g>
</svg>
<div class="cap"><b>The implicit pipeline.</b> Hard-coded one-to-one. Fine until a board needs to produce more than one firmware, or run on more than one OS.</div>
</div>
<!-- ===================== 2 ===================== -->
<h2><span class="n">02</span>The two things that broke it</h2>
<h3>A. The same board now ships many <em>kinds</em> of firmware</h3>
<p>As PX4 moves to per-vehicle-type firmware, one board legitimately produces a copter build, a rover build, a developer build, a bootloader. These differ along <strong>different, independent axes</strong> &mdash; vehicle type is not the same kind of choice as "developer vs production." Flattening them into one label namespace means the axes collide, and the list of files explodes combinatorially.</p>
<h3>B. The same board now ships firmware for more than one <em>runtime</em></h3>
<p>This is the one nobody has named out loud, and it's the most important. <strong>A board like VOXL2 already produces two firmwares from two different RTOSes</strong> &mdash; QuRT on its onboard DSP, and a Linux/POSIX build on its application processor &mdash; and bundles them together. PX4 is not a single-RTOS project waiting for Zephyr; it already runs on NuttX, QuRT, and a Linux/POSIX runtime today. The current model just can't <em>say</em> any of that, so VOXL2's two-RTOS reality is faked with hand-written build dependencies and a metadata file the build system doesn't even read.</p>
<div class="callout key">
<b>The hidden truth:</b> PX4 is <strong>already multi-RTOS.</strong> NuttX, QuRT (the DSP RTOS), and Linux/POSIX are all in the tree right now, and VOXL2 already ships two of them from one board. The "one board &rarr; many artifacts on many runtimes" world isn't a Zephyr hypothetical &mdash; it's the present, just unnamed. Zephyr doesn't introduce multi-RTOS; it would be the <em>fourth</em> runtime, and it's what finally makes ignoring the axis impossible.
</div>
<!-- ===================== 3 ===================== -->
<h2><span class="n">03</span>The model we want: a build is a point in a space</h2>
<p>Instead of "a folder and a label," a build is the intersection of <strong>four independent axes</strong>. Each axis answers one question, and only one. You pick a value on each axis, and that point in the space <em>is</em> the build.</p>
<div class="fig">
<svg viewBox="0 0 800 430" role="img" aria-label="Four independent axes combining into a single build">
<defs>
<marker id="ar2" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#58a6ff"/>
</marker>
</defs>
<!-- four axis columns -->
<g font-size="13">
<!-- board -->
<rect x="20" y="14" width="175" height="150" rx="9" fill="rgba(188,140,255,.06)" stroke="#bc8cff"/>
<text x="107" y="38" text-anchor="middle" class="lbl" fill="#bc8cff" font-size="14">BOARD</text>
<text x="107" y="56" text-anchor="middle" class="sub" font-size="11">what hardware</text>
<text x="107" y="84" text-anchor="middle" class="sub mono">fmu-v6x</text>
<text x="107" y="104" text-anchor="middle" class="sub mono">fmu-v5x</text>
<text x="107" y="124" text-anchor="middle" class="sub mono">voxl2</text>
<text x="107" y="144" text-anchor="middle" class="sub mono">cube-orange</text>
<!-- runtime -->
<rect x="215" y="14" width="175" height="150" rx="9" fill="rgba(248,81,73,.06)" stroke="#f85149"/>
<text x="302" y="38" text-anchor="middle" class="lbl" fill="#f85149" font-size="14">RUNTIME</text>
<text x="302" y="56" text-anchor="middle" class="sub" font-size="11">what OS it runs on</text>
<text x="302" y="84" text-anchor="middle" class="sub mono">NuttX</text>
<text x="302" y="104" text-anchor="middle" class="sub mono">QuRT (DSP)</text>
<text x="302" y="124" text-anchor="middle" class="sub mono">Linux / POSIX</text>
<text x="302" y="144" text-anchor="middle" class="sub mono">Zephyr</text>
<!-- vehicle -->
<rect x="410" y="14" width="175" height="150" rx="9" fill="rgba(63,185,80,.06)" stroke="#3fb950"/>
<text x="497" y="38" text-anchor="middle" class="lbl" fill="#3fb950" font-size="14">VEHICLE TYPE</text>
<text x="497" y="56" text-anchor="middle" class="sub" font-size="11">what it flies as</text>
<text x="497" y="84" text-anchor="middle" class="sub mono">copter</text>
<text x="497" y="104" text-anchor="middle" class="sub mono">fixedwing</text>
<text x="497" y="124" text-anchor="middle" class="sub mono">rover</text>
<text x="497" y="144" text-anchor="middle" class="sub mono">vtol</text>
<!-- variant -->
<rect x="605" y="14" width="175" height="150" rx="9" fill="rgba(88,166,255,.06)" stroke="#58a6ff"/>
<text x="692" y="38" text-anchor="middle" class="lbl" fill="#58a6ff" font-size="14">BUILD VARIANT</text>
<text x="692" y="56" text-anchor="middle" class="sub" font-size="11">what flavor</text>
<text x="692" y="84" text-anchor="middle" class="sub mono">production</text>
<text x="692" y="104" text-anchor="middle" class="sub mono">developer</text>
<text x="692" y="124" text-anchor="middle" class="sub mono">performance</text>
<text x="692" y="144" text-anchor="middle" class="sub mono">debug</text>
</g>
<!-- converging lines -->
<g stroke="#58a6ff" stroke-width="1.5" fill="none" opacity="0.7">
<path d="M107 164 L107 188 L400 188"/>
<path d="M302 164 L302 188"/>
<path d="M497 164 L497 188"/>
<path d="M692 164 L692 188 L400 188"/>
<line x1="400" y1="188" x2="400" y2="206" marker-end="url(#ar2)"/>
</g>
<!-- equation -->
<rect x="180" y="214" width="440" height="40" rx="8" fill="#1c2128" stroke="#58a6ff"/>
<text x="400" y="239" text-anchor="middle" class="lbl" font-size="14">a build = ( board , runtime , vehicle , variant )</text>
<!-- examples -->
<g class="mono" font-size="12.5">
<text x="400" y="290" text-anchor="middle" fill="#c9d1d9">( fmu-v6x , <tspan fill="#f85149">NuttX</tspan> , <tspan fill="#3fb950">copter</tspan> , production )</text>
<text x="400" y="312" text-anchor="middle" fill="#c9d1d9">( fmu-v6x , <tspan fill="#f85149">Zephyr</tspan> , <tspan fill="#3fb950">copter</tspan> , production )</text>
<text x="400" y="330" text-anchor="middle" class="sub" font-size="11">↑ same drone, different OS — one point apart on the runtime axis</text>
<text x="400" y="362" text-anchor="middle" fill="#c9d1d9">( voxl2 , <tspan fill="#f85149">Linux</tspan> , <tspan fill="#3fb950">copter</tspan> , production )</text>
<text x="400" y="384" text-anchor="middle" fill="#c9d1d9">( voxl2 , <tspan fill="#f85149">QuRT</tspan> , — , production )</text>
<text x="400" y="402" text-anchor="middle" class="sub" font-size="11">↑ one board, two RTOSes, shipped together — this exists today</text>
</g>
</svg>
<div class="cap"><b>Four axes, chosen independently.</b> The power is in the independence: "copter" is the same choice whether it runs on NuttX, QuRT, Linux, or Zephyr. Today the same vehicle on two runtimes means two unrelated files with copy-pasted contents (exactly how VOXL2's QuRT and Linux builds work now). Here they're one point on the vehicle axis, combined with two different points on the runtime axis.</div>
</div>
<h3>What each axis is responsible for</h3>
<table>
<tr><th>Axis</th><th>Answers</th><th>Owns</th><th>Owned by</th></tr>
<tr><td><b style="color:var(--purple)">Board</b></td><td>Which physical hardware?</td><td>Pins, sensors, memory, the base hardware definition</td><td>Manufacturer</td></tr>
<tr><td><b style="color:var(--red)">Runtime</b></td><td>Which OS / kernel?</td><td>The OS config world, the toolchain, the output format</td><td>PX4 core + RTOS</td></tr>
<tr><td><b style="color:var(--green)">Vehicle type</b></td><td>What does it fly as?</td><td>Which flight modules are on (the controllers, estimators)</td><td>PX4 defines canonical · vendor tunes</td></tr>
<tr><td><b style="color:var(--accent)">Build variant</b></td><td>Which flavor?</td><td>Dev/debug/perf toggles on top of everything else</td><td>Developers</td></tr>
</table>
<p>The right-hand column is the quiet payoff. <strong>Different people own different axes.</strong> A manufacturer cares about the board axis and shouldn't have to understand flight controllers. A developer adding a debug flavor shouldn't have to touch a vendor's board file. Separating the axes separates the ownership, which is what lets people customize at their own level without stepping on each other. The vehicle axis is the subtle one &mdash; it has <em>two</em> owners, and the next section is about how that works without conflict.</p>
<h3>The vehicle axis has a canonical definition, and a vendor delta</h3>
<p>"Multicopter" can't mean whatever each vendor wants it to mean &mdash; this is flight-control firmware, and the definition of a vehicle type is a contract. So the vehicle axis works in two parts that are always both present:</p>
<ul>
<li><strong>PX4 owns the canonical definition.</strong> There is exactly one authoritative "this is what PX4 means by a copter" &mdash; the reference set of flight modules, estimators, controllers. It lives in PX4 core, and it is the same for every board.</li>
<li><strong>The vendor tunes it in tree, as a delta on top.</strong> A manufacturer can adjust the canonical for their specific board &mdash; their board needs a different mixer, an extra driver, a tweaked default &mdash; from <em>their own board space</em>, never by editing or copying PX4's definition. The delta sits above the canonical and only changes what it explicitly names.</li>
</ul>
<div class="fig">
<svg viewBox="0 0 800 250" role="img" aria-label="Canonical PX4 vehicle definition with a vendor delta layered on top">
<defs>
<marker id="arc" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#8b949e"/>
</marker>
</defs>
<!-- canonical -->
<rect x="40" y="40" width="300" height="150" rx="10" fill="rgba(63,185,80,.08)" stroke="#3fb950" stroke-width="1.5"/>
<text x="60" y="68" class="lbl" fill="#3fb950" font-size="14">PX4 canonical · copter</text>
<text x="60" y="90" class="sub" font-size="11.5">the authoritative definition — one for all boards</text>
<g class="sub mono" font-size="12">
<text x="60" y="118">mc_pos_control</text>
<text x="60" y="138">mc_att_control</text>
<text x="60" y="158">mc_rate_control</text>
<text x="60" y="178">ekf2 · land_detector · …</text>
</g>
<!-- plus -->
<text x="370" y="120" text-anchor="middle" class="lbl" font-size="22" fill="#8b949e">+</text>
<!-- vendor delta -->
<rect x="400" y="60" width="220" height="110" rx="10" fill="rgba(188,140,255,.08)" stroke="#bc8cff" stroke-width="1.5"/>
<text x="420" y="86" class="lbl" fill="#bc8cff" font-size="13">vendor delta</text>
<text x="420" y="106" class="sub" font-size="11">(in the vendor's board space)</text>
<g class="sub mono" font-size="12">
<text x="420" y="132" fill="#3fb950">+ my_custom_mixer</text>
<text x="420" y="152" fill="#f85149">− land_detector</text>
</g>
<!-- equals -->
<line x1="630" y1="115" x2="666" y2="115" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arc)"/>
<text x="724" y="104" text-anchor="middle" class="lbl" font-size="13">this board's</text>
<text x="724" y="122" text-anchor="middle" class="lbl" font-size="13">copter</text>
<!-- guarantee strip -->
<text x="400" y="222" text-anchor="middle" class="sub" font-size="12">PX4's definition is never edited or copied — it shows through everywhere the vendor didn't override</text>
</svg>
<div class="cap"><b>Canonical + delta, never replacement.</b> PX4's copter is the contract; the vendor's board gets <i>that</i>, plus a small named delta. The vendor can't redefine "copter" out from under PX4 &mdash; they can only adjust it for their hardware, in tree, upstreamable. When PX4 changes the canonical, every vendor's board inherits the change automatically except where they explicitly diverged.</div>
</div>
<!-- ===================== 4 ===================== -->
<h2><span class="n">04</span>Two axes compose by stacking; one axis switches the whole backend</h2>
<p>The axes are not all the same <em>kind</em> of thing, and this is the crux of the design. Three of them are <strong>config that layers</strong>. One of them &mdash; runtime &mdash; <strong>swaps the machine that turns config into firmware.</strong></p>
<h3>Board, vehicle, variant: a layer stack</h3>
<p>These three just add and override settings, in order. Each layer is a thin delta on the one below it. You read it top to bottom and the bottom wins.</p>
<div class="fig">
<svg viewBox="0 0 800 280" role="img" aria-label="Layer stack composing into one resolved config">
<defs>
<marker id="ar3" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#8b949e"/>
</marker>
</defs>
<!-- stack -->
<g font-size="13.5">
<rect x="60" y="18" width="430" height="44" rx="7" fill="rgba(88,166,255,.10)" stroke="#58a6ff"/>
<text x="80" y="45" class="lbl" fill="#58a6ff">build variant</text>
<text x="470" y="45" text-anchor="end" class="sub mono" font-size="12">developer / debug</text>
<rect x="60" y="68" width="430" height="44" rx="7" fill="rgba(63,185,80,.10)" stroke="#3fb950"/>
<text x="80" y="95" class="lbl" fill="#3fb950">vehicle type</text>
<text x="470" y="95" text-anchor="end" class="sub mono" font-size="12">copter / rover</text>
<rect x="60" y="118" width="430" height="44" rx="7" fill="rgba(188,140,255,.10)" stroke="#bc8cff"/>
<text x="80" y="145" class="lbl" fill="#bc8cff">board</text>
<text x="470" y="145" text-anchor="end" class="sub mono" font-size="12">fmu-v6x sensors &amp; pins</text>
<rect x="60" y="168" width="430" height="44" rx="7" fill="#1c2128" stroke="#30363d"/>
<text x="80" y="195" class="lbl">default base</text>
<text x="470" y="195" text-anchor="end" class="sub mono" font-size="12">shared by everything</text>
</g>
<!-- specificity bracket -->
<text x="275" y="246" text-anchor="middle" class="sub" font-size="12">thinnest / most specific at top &nbsp;·&nbsp; broadest at bottom &nbsp;·&nbsp; bottom-up, last wins</text>
<!-- arrow to result -->
<line x1="490" y1="115" x2="560" y2="115" stroke="#8b949e" stroke-width="1.5" marker-end="url(#ar3)"/>
<rect x="565" y="86" width="215" height="58" rx="9" fill="#1c2128" stroke="#f0f6fc"/>
<text x="672" y="111" text-anchor="middle" class="lbl" font-size="13">one resolved</text>
<text x="672" y="130" text-anchor="middle" class="lbl" font-size="13">set of settings</text>
</svg>
<div class="cap"><b>Composition by layering.</b> A copter build is just <i>board + the copter layer</i>. A copter debug build is <i>board + copter + debug</i>. No file says "copter debug" &mdash; it's composed on demand. This is what kills the combinatorial file explosion.</div>
</div>
<h3>Runtime: a pluggable backend, not a layer</h3>
<p>The runtime axis is different in kind. Choosing one runtime over another &mdash; NuttX, QuRT, Linux, Zephyr &mdash; doesn't add a few settings; it chooses an <em>entirely separate configuration world</em> (its own OS config, its own toolchain, its own way of packaging the final image). You can't stack that; you have to hand off to the right backend. PX4 already does this for three runtimes today, just without ever naming the axis.</p>
<div class="fig">
<svg viewBox="0 0 800 350" role="img" aria-label="Runtime dispatch to per-RTOS backends">
<defs>
<marker id="ar4" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#8b949e"/>
</marker>
</defs>
<!-- top: resolved modules -->
<rect x="225" y="14" width="350" height="46" rx="8" fill="#1c2128" stroke="#30363d"/>
<text x="400" y="34" text-anchor="middle" class="lbl" font-size="13">vehicle + variant layers</text>
<text x="400" y="52" text-anchor="middle" class="sub" font-size="11">which flight modules — runtime-agnostic</text>
<line x1="400" y1="60" x2="400" y2="84" stroke="#8b949e" stroke-width="1.5" marker-end="url(#ar4)"/>
<!-- dispatch -->
<rect x="305" y="88" width="190" height="48" rx="8" fill="rgba(248,81,73,.10)" stroke="#f85149"/>
<text x="400" y="109" text-anchor="middle" class="lbl" fill="#f85149" font-size="13">RUNTIME dispatch</text>
<text x="400" y="126" text-anchor="middle" class="sub" font-size="11">pick the backend for this build</text>
<!-- branch lines -->
<g stroke="#f85149" stroke-width="1.5" fill="none" opacity="0.6">
<path d="M400 136 L400 158 L100 158 L100 180"/>
<path d="M400 158 L300 158 L300 180"/>
<path d="M400 158 L500 158 L500 180"/>
<path d="M400 136 L400 158 L700 158 L700 180"/>
</g>
<!-- four backends -->
<g font-size="13">
<rect x="20" y="182" width="160" height="120" rx="8" fill="#161b22" stroke="#f85149"/>
<text x="100" y="205" text-anchor="middle" class="lbl" fill="#f85149">NuttX</text>
<text x="100" y="230" text-anchor="middle" class="sub" font-size="11">own config</text>
<text x="100" y="248" text-anchor="middle" class="sub" font-size="11">+ toolchain</text>
<text x="100" y="266" text-anchor="middle" class="sub" font-size="11">+ .px4 wrap</text>
<text x="100" y="292" text-anchor="middle" class="sub mono" font-size="11">exists</text>
<rect x="220" y="182" width="160" height="120" rx="8" fill="#161b22" stroke="#f85149"/>
<text x="300" y="205" text-anchor="middle" class="lbl" fill="#f85149">Linux</text>
<text x="300" y="230" text-anchor="middle" class="sub" font-size="11">runs on</text>
<text x="300" y="248" text-anchor="middle" class="sub" font-size="11">host kernel</text>
<text x="300" y="266" text-anchor="middle" class="sub" font-size="11">+ installer</text>
<text x="300" y="292" text-anchor="middle" class="sub mono" font-size="11">exists</text>
<rect x="420" y="182" width="160" height="120" rx="8" fill="#161b22" stroke="#f85149"/>
<text x="500" y="205" text-anchor="middle" class="lbl" fill="#f85149">QuRT</text>
<text x="500" y="230" text-anchor="middle" class="sub" font-size="11">RTOS on</text>
<text x="500" y="248" text-anchor="middle" class="sub" font-size="11">the DSP</text>
<text x="500" y="266" text-anchor="middle" class="sub" font-size="11">+ bridge</text>
<text x="500" y="292" text-anchor="middle" class="sub mono" font-size="11">exists</text>
<rect x="620" y="182" width="160" height="120" rx="8" fill="rgba(63,185,80,.08)" stroke="#3fb950"/>
<text x="700" y="205" text-anchor="middle" class="lbl" fill="#3fb950">Zephyr</text>
<text x="700" y="230" text-anchor="middle" class="sub" font-size="11">own config</text>
<text x="700" y="248" text-anchor="middle" class="sub" font-size="11">+ devicetree</text>
<text x="700" y="266" text-anchor="middle" class="sub" font-size="11">+ image format</text>
<text x="700" y="292" text-anchor="middle" class="mono" font-size="11" fill="#3fb950">the new one</text>
</g>
</svg>
<div class="cap"><b>The seam.</b> The flight-module selection is decided <i>once</i>, runtime-agnostic. Then a per-runtime backend turns it into real firmware. Three backends already exist and ship today (NuttX, QuRT, Linux) &mdash; they're just not described as interchangeable. Adding Zephyr means adding a fourth backend behind the same seam, not rebuilding the system. The seam is already there; PX4 just never named it.</div>
</div>
<!-- ===================== 5 ===================== -->
<h2><span class="n">05</span>One board can be many builds, bundled</h2>
<p>Once a build is a point in the space, "one board produces several builds" is natural rather than special. A board simply <em>declares the set of points it ships</em>, and how they package together.</p>
<div class="fig">
<svg viewBox="0 0 800 320" role="img" aria-label="A board declaring multiple builds bundled into a package">
<defs>
<marker id="ar5" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#8b949e"/>
</marker>
</defs>
<!-- VOXL2 group -->
<text x="24" y="28" class="lbl" font-size="13">BOARD: voxl2</text>
<rect x="24" y="40" width="430" height="44" rx="7" fill="#1c2128" stroke="#30363d"/>
<text x="40" y="67" class="mono" font-size="12.5" fill="#c9d1d9">( voxl2 , <tspan fill="#f85149">Linux</tspan> , <tspan fill="#3fb950">copter</tspan> , production )</text>
<rect x="24" y="90" width="430" height="44" rx="7" fill="#1c2128" stroke="#30363d"/>
<text x="40" y="117" class="mono" font-size="12.5" fill="#c9d1d9">( voxl2 , <tspan fill="#f85149">QuRT</tspan> , — , production )</text>
<!-- arrows to artifacts -->
<line x1="454" y1="62" x2="500" y2="62" stroke="#8b949e" stroke-width="1.5" marker-end="url(#ar5)"/>
<line x1="454" y1="112" x2="500" y2="112" stroke="#8b949e" stroke-width="1.5" marker-end="url(#ar5)"/>
<text x="510" y="58" class="sub" font-size="11.5">app-processor</text>
<text x="510" y="73" class="sub" font-size="11.5">firmware</text>
<text x="510" y="108" class="sub" font-size="11.5">DSP (QuRT)</text>
<text x="510" y="123" class="sub" font-size="11.5">firmware</text>
<!-- bundle -->
<path d="M640 62 L660 62 L660 112 L640 112" fill="none" stroke="#8b949e" stroke-width="1.5"/>
<line x1="660" y1="87" x2="690" y2="87" stroke="#8b949e" stroke-width="1.5" marker-end="url(#ar5)"/>
<rect x="694" y="66" width="86" height="42" rx="8" fill="rgba(210,153,34,.10)" stroke="#d29922"/>
<text x="737" y="92" text-anchor="middle" class="lbl" fill="#d29922">.deb</text>
<text x="640" y="150" text-anchor="middle" class="sub" font-size="11">bundle into one package</text>
<line x1="24" y1="180" x2="780" y2="180" stroke="#30363d"/>
<!-- fmu-v6x group -->
<text x="24" y="208" class="lbl" font-size="13">BOARD: fmu-v6x <tspan class="sub" font-size="11">(after Zephyr lands)</tspan></text>
<rect x="24" y="220" width="430" height="44" rx="7" fill="#1c2128" stroke="#30363d"/>
<text x="40" y="247" class="mono" font-size="12.5" fill="#c9d1d9">( fmu-v6x , <tspan fill="#f85149">NuttX</tspan> , <tspan fill="#3fb950">copter</tspan> , production )</text>
<rect x="24" y="270" width="430" height="44" rx="7" fill="#1c2128" stroke="#30363d"/>
<text x="40" y="297" class="mono" font-size="12.5" fill="#c9d1d9">( fmu-v6x , <tspan fill="#f85149">Zephyr</tspan> , <tspan fill="#3fb950">copter</tspan> , production )</text>
<line x1="454" y1="242" x2="500" y2="242" stroke="#8b949e" stroke-width="1.5" marker-end="url(#ar5)"/>
<line x1="454" y1="292" x2="500" y2="292" stroke="#8b949e" stroke-width="1.5" marker-end="url(#ar5)"/>
<text x="510" y="246" class="lbl mono" font-size="12">.px4</text>
<text x="510" y="296" class="lbl mono" font-size="12">Zephyr image</text>
<text x="600" y="270" class="sub" font-size="11">same drone, same flight</text>
<text x="600" y="286" class="sub" font-size="11">modules, two runtimes —</text>
<text x="600" y="302" class="sub" font-size="11">user picks one</text>
</svg>
<div class="cap"><b>Multi-artifact is just a list of points + a bundling rule.</b> VOXL2 stops being a special case wired by hand and becomes a board that declares two points and bundles them. The firmware catalog the user sees is then a faithful readout of these declarations &mdash; not a tool guessing intent from filenames.</div>
</div>
<!-- ===================== 6 ===================== -->
<h2><span class="n">06</span>Why this is the right shape</h2>
<table>
<tr><th>Symptom today</th><th>Root cause</th><th>Resolved by</th></tr>
<tr><td>Label means four things; tools guess a build's purpose from its name</td><td>One axis carrying four questions</td><td>Four explicit axes</td></tr>
<tr><td>File count explodes as we add vehicle types</td><td>Every combination needs its own file</td><td>Layers compose on demand</td></tr>
<tr><td>VOXL2 wired with hand-written build edges + an unread metadata file</td><td>No concept of multi-artifact</td><td>A board declares its set of points</td></tr>
<tr><td>Manufacturers and developers edit the same files and collide</td><td>Ownership not separated</td><td>Each axis has an owner</td></tr>
<tr><td>Already on three RTOSes, but no clean path to add a fourth without forking the build</td><td>Runtime is real but unnamed &mdash; baked in per board, not a pluggable axis</td><td>Runtime = a backend behind a seam</td></tr>
</table>
<p>Every mature embedded build system that solved this solved it the same way: <strong>separate the axes, compose config by layering, and put a backend behind each runtime.</strong> We're not inventing a model &mdash; we're adopting a known-good one and admitting the axes we already have.</p>
<!-- ===================== 7 ===================== -->
<h2><span class="n">07</span>What this does to the build target name</h2>
<p>The model is abstract until you see it in the one string every developer types. Today the target name is <code>vendor_board_label</code> &mdash; and that single <code>label</code> slot is exactly the overloaded knob from section 1. You cannot tell, from the name alone, which axis you're even on:</p>
<div class="fig">
<svg viewBox="0 0 800 250" role="img" aria-label="Today's flat target names, color-coded by which hidden axis each label is">
<text x="24" y="26" class="lbl" font-size="13">TODAY &mdash; one flat label, four hidden meanings</text>
<g class="mono" font-size="14">
<text x="24" y="62" fill="#c9d1d9">px4_fmu-v6x_<tspan fill="#3fb950">multicopter</tspan></text>
<text x="24" y="90" fill="#c9d1d9">px4_fmu-v6x_<tspan fill="#3fb950">rover</tspan></text>
<text x="24" y="118" fill="#c9d1d9">px4_fmu-v6x_<tspan fill="#58a6ff">zenoh</tspan></text>
<text x="24" y="146" fill="#c9d1d9">px4_fmu-v6x_<tspan fill="#58a6ff">mavlink-dev</tspan></text>
<text x="24" y="174" fill="#c9d1d9">px4_fmu-v6x_<tspan fill="#d29922">bootloader</tspan></text>
<text x="24" y="202" fill="#c9d1d9">modalai_voxl2_<tspan fill="#f85149">slpi</tspan></text>
<text x="24" y="230" fill="#c9d1d9">modalai_voxl2_<tspan fill="#8b949e">default</tspan></text>
</g>
<!-- annotations -->
<g font-size="12.5">
<text x="430" y="62" class="sub">&larr; <tspan fill="#3fb950">a vehicle type</tspan></text>
<text x="430" y="90" class="sub">&larr; <tspan fill="#3fb950">a vehicle type</tspan></text>
<text x="430" y="118" class="sub">&larr; <tspan fill="#58a6ff">a build variant</tspan></text>
<text x="430" y="146" class="sub">&larr; <tspan fill="#58a6ff">a build variant</tspan></text>
<text x="430" y="174" class="sub">&larr; <tspan fill="#d29922">a bootloader</tspan></text>
<text x="430" y="202" class="sub">&larr; <tspan fill="#f85149">a whole RTOS (QuRT)!</tspan></text>
<text x="430" y="230" class="sub">&larr; <tspan fill="#8b949e">RTOS? vehicle? both? unknown</tspan></text>
</g>
</svg>
<div class="cap"><b>The name lies.</b> <code>multicopter</code> and <code>zenoh</code> sit in the same slot but answer different questions. <code>voxl2_slpi</code> hides an entire RTOS switch inside a "label." <code>voxl2_default</code> tells you nothing about what it flies as or what OS it runs. The name can't express the build because the model behind it has only one axis.</div>
</div>
<p>Since the build system is being reset anyway, the target name is reset with it &mdash; no carried-over <code>_</code>/<code>-</code> blob, just one dotted field per axis. Vendor becomes its own axis (it was always hiding inside <code>px4_fmu-v6x</code>), so the name reads <code>vendor.board.runtime.vehicle.variant</code>. Each field is a coordinate; the boring defaults stay invisible until you need them:</p>
<div class="fig">
<svg viewBox="0 0 800 462" role="img" aria-label="Proposed target name with explicit axis fields">
<!-- schema -->
<text x="24" y="26" class="lbl" font-size="13">PROPOSED &mdash; a name is a point on the five axes</text>
<g class="mono" font-size="15">
<text x="24" y="62" fill="#c9d1d9">
<tspan fill="#e3b341">px4</tspan><tspan fill="#8b949e">.</tspan><tspan fill="#bc8cff">fmu-v6x</tspan><tspan fill="#8b949e">.</tspan><tspan fill="#f85149">nuttx</tspan><tspan fill="#8b949e">.</tspan><tspan fill="#3fb950">copter</tspan><tspan fill="#8b949e">.</tspan><tspan fill="#58a6ff">debug</tspan>
</text>
</g>
<g font-size="11.5" class="sub">
<text x="24" y="84" fill="#e3b341">vendor</text>
<text x="78" y="84" fill="#bc8cff">board</text>
<text x="158" y="84" fill="#f85149">runtime</text>
<text x="222" y="84" fill="#3fb950">vehicle</text>
<text x="290" y="84" fill="#58a6ff">variant</text>
</g>
<line x1="24" y1="104" x2="776" y2="104" stroke="#30363d"/>
<text x="24" y="132" class="sub" font-size="12.5">a clean reset &mdash; every axis is its own dotted field, and the boring defaults can be omitted:</text>
<g class="mono" font-size="13.5">
<text x="24" y="162" fill="#c9d1d9"><tspan fill="#e3b341">px4</tspan>.<tspan fill="#bc8cff">fmu-v6x</tspan>.<tspan fill="#f85149">nuttx</tspan>.<tspan fill="#3fb950">copter</tspan></text>
<text x="560" y="162" class="sub" font-size="11.5">variant defaults to production</text>
<text x="24" y="190" fill="#c9d1d9"><tspan fill="#e3b341">px4</tspan>.<tspan fill="#bc8cff">fmu-v6x</tspan>.<tspan fill="#f85149">nuttx</tspan>.<tspan fill="#3fb950">rover</tspan></text>
<text x="24" y="218" fill="#c9d1d9"><tspan fill="#e3b341">px4</tspan>.<tspan fill="#bc8cff">fmu-v6x</tspan>.<tspan fill="#f85149">zephyr</tspan>.<tspan fill="#3fb950">copter</tspan></text>
<text x="560" y="218" class="sub" font-size="11.5">same drone, different RTOS &mdash; obvious now</text>
<text x="24" y="246" fill="#c9d1d9"><tspan fill="#e3b341">px4</tspan>.<tspan fill="#bc8cff">fmu-v6x</tspan>.<tspan fill="#f85149">nuttx</tspan>.<tspan fill="#3fb950">copter</tspan>.<tspan fill="#58a6ff">zenoh</tspan></text>
<text x="560" y="246" class="sub" font-size="11.5">variant stacks on a vehicle, no new file</text>
<text x="24" y="274" fill="#c9d1d9"><tspan fill="#e3b341">modalai</tspan>.<tspan fill="#bc8cff">voxl2</tspan>.<tspan fill="#f85149">qurt</tspan>.<tspan fill="#3fb950">copter</tspan></text>
<text x="560" y="274" class="sub" font-size="11.5">DSP build &mdash; carries the flight stack</text>
<text x="24" y="300" fill="#c9d1d9"><tspan fill="#e3b341">modalai</tspan>.<tspan fill="#bc8cff">voxl2</tspan>.<tspan fill="#f85149">linux</tspan></text>
<text x="560" y="300" class="sub" font-size="11.5">Linux companion &mdash; vehicle-less bridge</text>
</g>
<defs>
<marker id="arbundle" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#d29922"/>
</marker>
</defs>
<!-- bundle target on its own row, indented to read as "produced from the two above" -->
<path d="M30 308 V330 H44" fill="none" stroke="#d29922" stroke-width="1.3" marker-end="url(#arbundle)"/>
<g class="mono" font-size="13.5">
<text x="56" y="335" fill="#c9d1d9"><tspan fill="#e3b341">modalai</tspan>.<tspan fill="#bc8cff">voxl2</tspan>.<tspan fill="#d29922">bundle</tspan>.<tspan fill="#3fb950">copter</tspan></text>
</g>
<text x="560" y="335" class="sub" font-size="11.5" fill="#d29922"><tspan fill="#d29922">bundle</tspan> = composite runtime: both components + package</text>
<!-- peripheral row, set apart -->
<line x1="24" y1="372" x2="776" y2="372" stroke="#30363d" stroke-dasharray="3 3"/>
<g class="mono" font-size="13.5">
<text x="24" y="402" fill="#c9d1d9"><tspan fill="#e3b341">holybro</tspan>.<tspan fill="#bc8cff">can-gps-v1</tspan>.<tspan fill="#f85149">nuttx</tspan>.<tspan fill="#d29922">peripheral</tspan></text>
<text x="560" y="402" class="sub" font-size="11.5">a CAN GPS sensor node</text>
</g>
<text x="24" y="432" class="sub" font-size="11.5">a peripheral has <tspan fill="#d29922">no vehicle</tspan> — it doesn't fly anything. The vehicle slot is a <tspan fill="#d29922">peripheral kind</tspan> instead,</text>
<text x="24" y="448" class="sub" font-size="11.5">declared explicitly — not inferred from a <tspan class="mono">ROMFSROOT="cannode"</tspan> string buried in the board's defaults.</text>
</svg>
<div class="cap"><b>The name becomes the model.</b> <code>vendor.board.runtime.vehicle.variant</code> &mdash; each dot is an axis, each axis is independent, and unset axes fall back to canonical defaults. <code>px4.fmu-v6x.nuttx.copter</code> vs <code>px4.fmu-v6x.zephyr.copter</code> shows the whole model in one field of difference. The two VOXL2 component builds are the building blocks; <code>modalai.voxl2.bundle.copter</code> is the <b>bundle target</b> a user actually builds &mdash; here <code>bundle</code> is a <i>composite runtime</i> that expands to both components (DSP + Linux), binds the <code>copter</code> vehicle to the flight-carrying one, and packages the result. The board declares what a bundle produces &mdash; a <code>.deb</code> for VOXL2 today, an <code>.rpm</code>, <code>.ipk</code>, or tarball for the next board &mdash; so the name carries intent, not format. And the peripheral &mdash; today a magic string you have to grep for (section 1) &mdash; becomes a first-class, self-describing target. <i>(The dot separator is illustrative; bootloader, like peripheral, is a declared artifact kind, not a vehicle or variant.)</i></div>
</div>
<!-- ===================== 8 ===================== -->
<h2><span class="n">08</span>How a name resolves into a build</h2>
<p>A target name is a coordinate; <em>resolving</em> it is the build system's job. The composer walks the axes in order and, for each one, finds the layer that applies by <strong>looking it up</strong> &mdash; the board's own directory first, falling back to PX4 core. The layer's <em>position</em> in the stack is fixed by its axis; its <em>source</em> is whatever the lookup finds. Compose the layers, hand the result to the runtime backend, get an artifact.</p>
<p>Three real targets, resolved end to end:</p>
<div class="fig">
<svg viewBox="0 0 800 520" role="img" aria-label="Three target names resolving through axis lookup into artifacts">
<defs>
<marker id="arrow8" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0 L10 5 L0 10 z" fill="#8b949e"/>
</marker>
</defs>
<!-- column headers -->
<text x="24" y="22" class="sub" font-size="11" letter-spacing=".05em">TARGET</text>
<text x="250" y="22" class="sub" font-size="11" letter-spacing=".05em">AXIS &rarr; LAYER SOURCE (board dir first, else core)</text>
<text x="660" y="22" class="sub" font-size="11" letter-spacing=".05em">BACKEND &rarr; ARTIFACT</text>
<line x1="24" y1="32" x2="776" y2="32" stroke="#30363d"/>
<!-- ===== Example 1: fmu-v6x copter ===== -->
<text x="24" y="62" class="mono" font-size="12.5" fill="#c9d1d9"><tspan fill="#e3b341">px4</tspan>.<tspan fill="#bc8cff">fmu-v6x</tspan></text>
<text x="24" y="80" class="mono" font-size="12.5" fill="#c9d1d9">.<tspan fill="#f85149">nuttx</tspan>.<tspan fill="#3fb950">copter</tspan></text>
<g font-size="11" class="mono">
<text x="250" y="56" fill="#bc8cff">board</text>
<text x="335" y="56" fill="#8b949e">&rarr; boards/px4/fmu-v6x/ <tspan fill="#8b949e" font-style="italic">(board base)</tspan></text>
<text x="250" y="74" fill="#f85149">runtime</text>
<text x="335" y="74" fill="#8b949e">&rarr; platforms/nuttx + board nuttx-config</text>
<text x="250" y="92" fill="#3fb950">vehicle</text>
<text x="335" y="92" fill="#8b949e">&rarr; <tspan fill="#3fb950">core canonical copter</tspan> <tspan font-style="italic">(no board delta)</tspan></text>
</g>
<line x1="600" y1="74" x2="650" y2="74" stroke="#8b949e" stroke-width="1.3" marker-end="url(#arrow8)"/>
<text x="658" y="70" class="mono" font-size="12" fill="#f85149">nuttx</text>
<text x="658" y="88" class="mono" font-size="12" fill="#c9d1d9">&rarr; .px4</text>
<text x="250" y="112" class="sub" font-size="10.5" font-style="italic">no overrides &mdash; pure inheritance from core</text>
<line x1="24" y1="128" x2="776" y2="128" stroke="#30363d" stroke-dasharray="2 3"/>
<!-- ===== Example 2: voxl2 bundle copter ===== -->
<text x="24" y="158" class="mono" font-size="12.5" fill="#c9d1d9"><tspan fill="#e3b341">modalai</tspan>.<tspan fill="#bc8cff">voxl2</tspan></text>
<text x="24" y="176" class="mono" font-size="12.5" fill="#c9d1d9">.<tspan fill="#d29922">bundle</tspan>.<tspan fill="#3fb950">copter</tspan></text>
<text x="24" y="200" class="sub" font-size="10" font-style="italic">composite runtime</text>
<g font-size="11" class="mono">
<text x="250" y="150" fill="#d29922">bundle expands to two components:</text>
<!-- component A -->
<text x="262" y="170" fill="#c9d1d9"><tspan fill="#f85149">qurt</tspan>.<tspan fill="#3fb950">copter</tspan> <tspan fill="#8b949e">&rarr; <tspan fill="#3fb950">core canonical copter</tspan> binds here</tspan></text>
<text x="274" y="186" fill="#8b949e">DSP &mdash; carries the flight stack</text>
<!-- component B -->
<text x="262" y="206" fill="#c9d1d9"><tspan fill="#f85149">linux</tspan> <tspan fill="#8b949e">&rarr; companion, vehicle-less (for now)</tspan></text>
<text x="274" y="222" fill="#8b949e">Linux apps bridge</text>
</g>
<line x1="600" y1="178" x2="650" y2="178" stroke="#8b949e" stroke-width="1.3" marker-end="url(#arrow8)"/>
<text x="658" y="166" class="mono" font-size="12" fill="#f85149">qurt</text>
<text x="658" y="182" class="mono" font-size="11" fill="#f85149">+ linux</text>
<text x="658" y="200" class="mono" font-size="12" fill="#d29922">&rarr; .deb</text>
<text x="250" y="244" class="sub" font-size="10.5" font-style="italic">vehicle binds to the flight-carrying component; bundle packages both</text>
<line x1="24" y1="260" x2="776" y2="260" stroke="#30363d" stroke-dasharray="2 3"/>
<!-- ===== Example 3: ark peripheral ===== -->
<text x="24" y="290" class="mono" font-size="12.5" fill="#c9d1d9"><tspan fill="#e3b341">ark</tspan>.<tspan fill="#bc8cff">can-gps</tspan></text>
<text x="24" y="308" class="mono" font-size="12.5" fill="#c9d1d9">.<tspan fill="#f85149">nuttx</tspan>.<tspan fill="#d29922">peripheral</tspan></text>
<g font-size="11" class="mono">
<text x="250" y="284" fill="#bc8cff">board</text>
<text x="335" y="284" fill="#8b949e">&rarr; boards/ark/can-gps/ <tspan font-style="italic">(board base)</tspan></text>
<text x="250" y="302" fill="#f85149">runtime</text>
<text x="335" y="302" fill="#8b949e">&rarr; platforms/nuttx + board nuttx-config</text>
<text x="250" y="320" fill="#d29922">vehicle</text>
<text x="335" y="320" fill="#8b949e">&rarr; <tspan fill="#d29922">peripheral kind</tspan> <tspan font-style="italic">(no flight stack)</tspan></text>
</g>
<line x1="600" y1="302" x2="650" y2="302" stroke="#8b949e" stroke-width="1.3" marker-end="url(#arrow8)"/>
<text x="658" y="298" class="mono" font-size="12" fill="#f85149">nuttx</text>
<text x="658" y="316" class="mono" font-size="11" fill="#c9d1d9">&rarr; cannode fw</text>
<text x="250" y="340" class="sub" font-size="10.5" font-style="italic">peripheral is a declared kind in the vehicle slot &mdash; replaces the ROMFSROOT magic string</text>
<!-- where layers live legend -->
<rect x="24" y="366" width="752" height="138" rx="10" fill="#1c2128" stroke="#30363d"/>
<text x="40" y="390" class="lbl" font-size="13">where each layer lives</text>
<text x="40" y="412" class="sub" font-size="11.5"><tspan fill="#3fb950" class="mono">PX4 core</tspan> &mdash; the shared, canonical layers, one definition for every board:</text>
<text x="60" y="432" class="sub mono" font-size="11.5">· canonical vehicles (<tspan fill="#3fb950">copter, rover, fixedwing&hellip;</tspan>) · shared variants (<tspan fill="#58a6ff">zenoh, debug&hellip;</tspan>) · runtime backends</text>
<text x="40" y="458" class="sub" font-size="11.5"><tspan fill="#bc8cff" class="mono">boards/&lt;vendor&gt;/&lt;board&gt;/</tspan> &mdash; what this board owns, all <tspan fill="#e3b341">additive deltas</tspan> on the above:</text>
<text x="60" y="478" class="sub mono" font-size="11.5">· board base & pins · vehicle deltas (tune canonical) · custom variants (board-specific) · bundle rule</text>
<text x="40" y="498" class="sub" font-size="10.5" font-style="italic">lookup order: board dir wins on the keys it names; everything else falls through to core. (folder names proposed)</text>
</svg>
<div class="cap"><b>Resolution is lookup, not duplication.</b> Each axis finds its layer by searching the board dir then core. <code>fmu-v6x</code> defines no copter delta, so it inherits core canonical copter whole. <code>voxl2</code>'s <code>bundle</code> is a composite runtime that expands to a flight-carrying DSP build (where <code>copter</code> binds) plus a vehicle-less Linux companion, packaged together. <code>ark.can-gps</code> has no vehicle at all &mdash; <code>peripheral</code> is a declared kind. <i>This makes VOXL2's case the same migration every board is getting: today its DSP build compiles every controller and picks the airframe at runtime &mdash; the exact <code>_default</code> legacy the vehicle axis replaces. In the model it becomes a real <code>copter</code> (or <code>rover</code>&hellip;) build like everywhere else.</i></div>
</div>
<p>The one rule that keeps this from rotting: a board's layer is a <strong>delta, never a copy</strong>. It states only what it adds or removes relative to the canonical &mdash; never a restatement of the whole thing. That is what stops boards from silently drifting as PX4 evolves the canonical underneath them:</p>
<div class="fig">
<svg viewBox="0 0 800 250" role="img" aria-label="A board vehicle delta file contains only added and removed keys, not a restatement">
<!-- core canonical (read-only reference) -->
<rect x="24" y="24" width="300" height="200" rx="10" fill="rgba(63,185,80,.06)" stroke="#3fb950"/>
<text x="44" y="48" class="lbl" fill="#3fb950" font-size="13">core canonical &middot; copter</text>
<text x="44" y="66" class="sub" font-size="10.5" font-style="italic">owned by PX4, evolves over time</text>
<g class="sub mono" font-size="11.5">
<text x="44" y="92">mc_pos_control</text>
<text x="44" y="112">mc_att_control</text>
<text x="44" y="132">mc_rate_control</text>
<text x="44" y="152">ekf2</text>
<text x="44" y="172">land_detector</text>
<text x="44" y="192">&hellip; (the full set)</text>
</g>
<!-- board delta file -->
<rect x="430" y="24" width="346" height="200" rx="10" fill="rgba(188,140,255,.06)" stroke="#bc8cff"/>
<text x="450" y="48" class="lbl" fill="#bc8cff" font-size="13">boards/&lt;vendor&gt;/acme-x/vehicles/copter</text>
<text x="450" y="66" class="sub" font-size="10.5" font-style="italic">the ENTIRE file &mdash; just the delta</text>
<g class="mono" font-size="12.5">
<text x="450" y="98" fill="#3fb950">+ acme_custom_esc_driver</text>
<text x="450" y="124" fill="#f85149">&minus; land_detector</text>
</g>
<text x="450" y="162" class="sub" font-size="11">everything else &mdash; the whole flight stack &mdash;</text>
<text x="450" y="180" class="sub" font-size="11">is <tspan fill="#3fb950">inherited</tspan>, not restated.</text>
<text x="450" y="204" class="sub" font-size="10.5" font-style="italic">PX4 improves mc_pos_control &rarr; this board gets it free.</text>
<!-- arrow -->
<line x1="328" y1="124" x2="426" y2="124" stroke="#8b949e" stroke-width="1.3" stroke-dasharray="4 3"/>
<text x="377" y="116" text-anchor="middle" class="sub" font-size="10.5">delta on</text>
</svg>
<div class="cap"><b>Additive, not a rewrite.</b> The board's <code>copter</code> file is two lines because that's the board's entire divergence from canonical. It never copies the flight stack, so when PX4 changes canonical copter, every board inherits the change except on the exact keys it overrode. Copy-the-whole-thing is what causes drift; deltas make drift structurally impossible. <i>(Folder/file shape is illustrative.)</i></div>
</div>
<h3>Two ways to customize: tune what exists, or add your own</h3>
<p>The delta above is one kind of customization &mdash; <em>tuning</em> a canonical layer PX4 already owns. But you can also <em>add</em> a layer that doesn't exist in core at all: a custom variant you own, for something experimental. Say you're building an onboard-AI flavor, <code>ai</code>. Both are the same composition; they differ only in whether the lookup finds an existing canonical to override, or a brand-new layer you authored:</p>
<div class="fig">
<svg viewBox="0 0 800 270" role="img" aria-label="Tune an existing canonical versus add a new custom variant">
<!-- TUNE -->
<rect x="24" y="30" width="360" height="190" rx="10" fill="rgba(63,185,80,.05)" stroke="#3fb950"/>
<text x="44" y="56" class="lbl" fill="#3fb950" font-size="13">TUNE an existing canonical</text>
<text x="44" y="74" class="sub" font-size="10.5" font-style="italic">override a layer PX4 owns</text>
<text x="44" y="102" class="mono sub" font-size="11.5">boards/&lt;v&gt;/acme-x/<tspan fill="#3fb950">vehicles/copter</tspan></text>
<g class="mono" font-size="12">
<text x="44" y="128" fill="#3fb950">+ acme_custom_esc</text>
<text x="44" y="148" fill="#f85149">&minus; land_detector</text>
</g>
<text x="44" y="178" class="sub mono" font-size="11.5">&rarr; px4.fmu-v6x.nuttx.<tspan fill="#3fb950">copter</tspan></text>
<text x="44" y="202" class="sub" font-size="10.5">the canonical <tspan fill="#3fb950">copter</tspan> exists in core; you bend it</text>
<!-- ADD -->
<rect x="416" y="30" width="360" height="190" rx="10" fill="rgba(88,166,255,.05)" stroke="#58a6ff"/>
<text x="436" y="56" class="lbl" fill="#58a6ff" font-size="13">ADD your own variant</text>
<text x="436" y="74" class="sub" font-size="10.5" font-style="italic">a net-new layer you author &amp; own</text>
<text x="436" y="102" class="mono sub" font-size="11.5">boards/&lt;v&gt;/acme-x/<tspan fill="#58a6ff">variants/ai</tspan></text>
<g class="mono" font-size="12">
<text x="436" y="128" fill="#3fb950">+ ai_inference_runtime</text>
<text x="436" y="148" fill="#3fb950">+ onboard_compute_bridge</text>
</g>
<text x="436" y="178" class="sub mono" font-size="11.5">&rarr; px4.fmu-v6x.nuttx.copter.<tspan fill="#58a6ff">ai</tspan></text>
<text x="436" y="198" class="sub mono" font-size="11.5">&rarr; px4.fmu-v6x.nuttx.rover.<tspan fill="#58a6ff">ai</tspan> <tspan class="sub" font-family="-apple-system">stacks on any class</tspan></text>
<text x="436" y="216" class="sub" font-size="10.5"><tspan fill="#58a6ff">ai</tspan> isn't in core &mdash; you own it, in your board dir</text>
<text x="400" y="252" text-anchor="middle" class="sub" font-size="11.5">both resolved by the <b style="fill:#f0f6fc">same lookup</b> &mdash; found in your board dir &rarr; you own it. canonical vs custom is just <i>where the lookup landed.</i></text>
</svg>
<div class="cap"><b>Tune or add &mdash; one mechanism.</b> Overriding canonical <code>copter</code> and authoring a brand-new <code>ai</code> variant are the same operation: a layer the composer finds in your board dir instead of core. Because the axes are orthogonal, your <code>ai</code> variant stacks onto <em>any</em> class &mdash; write it once, get <code>copter.ai</code>, <code>rover.ai</code>, <code>vtol.ai</code> for free. When your experiment matures, promoting it from board-owned to canonical is just moving the file from your board dir up to core. <i>(Folder shape illustrative.)</i></div>
</div>
<!-- ===================== 9 ===================== -->
<h2><span class="n">09</span>Where it all lives on disk</h2>
<p>This is the model made filesystem &mdash; and much of it isn't hypothetical. PR&nbsp;#27667 already introduced <code>target_classes/</code> as Kconfig fragments, so the canonical layer is real and stays Kconfig-native. The tree below shows the whole shape: what exists, plus what this proposal adds around it (the runtime axis, board-level custom variants, the bundle rule).</p>
<p style="color:var(--muted);font-size:14.5px;"><b style="color:var(--head)">One vocabulary note:</b> what this doc has called the <span style="color:var(--green)">vehicle</span> axis is more precisely a <b>target class</b> &mdash; because the same slot also holds non-flying kinds (<code>cannode</code>/peripheral, <code>io</code>, <code>sitl</code>). "Vehicle" is the common case; "target class" is the accurate name, and it's already #27667's term.</p>
<div class="fig">
<svg viewBox="0 0 800 560" role="img" aria-label="Proposed repository directory structure for the layered build model">
<style>
.tree { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: 12.5px; }
.dir { fill: #bc8cff; font-weight: 600; }
.core { fill: #3fb950; }
.rt { fill: #f85149; }
.var { fill: #58a6ff; }
.cmt { fill: #8b949e; }
.pth { fill: #c9d1d9; }
</style>
<g class="tree">
<text x="24" y="30" class="pth">PX4-Autopilot/</text>
<!-- target_classes -->
<text x="44" y="56" class="core">target_classes/</text>
<text x="300" y="56" class="cmt">canonical classes &mdash; Kconfig fragments <tspan fill="#3fb950">(exists, #27667)</tspan></text>
<text x="64" y="78" class="core">copter.px4board fixedwing.px4board vtol.px4board</text>
<text x="64" y="98" class="core">rover uuv spacecraft airship <tspan class="cmt">&hellip;flying</tspan></text>
<text x="64" y="118" class="core">cannode io sitl ros2 linux <tspan class="cmt">&hellip;non-flying classes</tspan></text>
<!-- variants -->
<text x="44" y="146" class="var">variants/</text>
<text x="300" y="146" class="cmt">canonical shared variants <tspan class="var">(proposed)</tspan></text>
<text x="64" y="166" class="var">zenoh mavlink-dev performance debug &hellip;</text>
<!-- platforms -->
<text x="44" y="194" class="rt">platforms/</text>
<text x="300" y="194" class="cmt">runtime backends <tspan class="cmt">(exists; zephyr proposed)</tspan></text>
<text x="64" y="214" class="rt">nuttx/ posix/ qurt/ <tspan fill="#3fb950">zephyr/</tspan></text>
<!-- boards -->
<text x="44" y="242" class="dir">boards/&lt;vendor&gt;/&lt;board&gt;/</text>
<text x="300" y="242" class="cmt">what this board owns &mdash; all additive deltas</text>
<text x="64" y="266" class="pth">base.px4board</text>
<text x="300" y="266" class="cmt">board base: pins, sensors, memory</text>
<text x="64" y="290" class="rt">nuttx-config/</text>
<text x="300" y="290" class="cmt">per-board RTOS config <tspan class="cmt">(exists)</tspan></text>
<text x="64" y="310" class="rt">zephyr-config/</text>
<text x="300" y="310" class="cmt">per-board Zephyr config <tspan fill="#3fb950">(proposed)</tspan></text>
<text x="64" y="334" class="core">vehicles/</text>
<text x="300" y="334" class="cmt">class DELTAS &mdash; tune canonical <tspan class="var">(proposed)</tspan></text>
<text x="84" y="354" class="core">copter</text>
<text x="300" y="354" class="cmt"><tspan fill="#3fb950">+ acme_esc</tspan> <tspan fill="#f85149">&minus; land_detector</tspan> (delta only)</text>
<text x="64" y="378" class="var">variants/</text>
<text x="300" y="378" class="cmt">CUSTOM variants &mdash; dev-owned <tspan class="var">(proposed)</tspan></text>
<text x="84" y="398" class="var">ai</text>
<text x="300" y="398" class="cmt"><tspan fill="#3fb950">+ ai_inference_runtime</tspan> (your experiment)</text>
<text x="64" y="422" class="pth">board.&lt;target&gt;</text>
<text x="300" y="422" class="cmt">declares artifacts + composite/bundle rule <tspan class="var">(proposed)</tspan></text>
</g>
<!-- separator -->
<line x1="24" y1="446" x2="776" y2="446" stroke="#30363d"/>
<!-- mapping strip: name field -> where it resolves -->
<text x="24" y="470" class="sub" font-size="11.5" letter-spacing=".04em">EACH NAME FIELD RESOLVES TO A PLACE ABOVE</text>
<g class="tree" font-size="12">
<text x="24" y="494" class="pth"><tspan fill="#e3b341">px4</tspan>.<tspan fill="#bc8cff">fmu-v6x</tspan>.<tspan fill="#f85149">nuttx</tspan>.<tspan fill="#3fb950">copter</tspan>.<tspan fill="#58a6ff">ai</tspan></text>
</g>
<g font-size="11" class="cmt">
<text x="24" y="516"><tspan fill="#e3b341">px4</tspan>/<tspan fill="#bc8cff">fmu-v6x</tspan> &rarr; boards/&lt;vendor&gt;/&lt;board&gt;/</text>
<text x="24" y="534"><tspan fill="#f85149">nuttx</tspan> &rarr; platforms/nuttx + board nuttx-config/</text>
<text x="410" y="516"><tspan fill="#3fb950">copter</tspan> &rarr; target_classes/copter <tspan class="cmt">(+ board vehicles/copter delta if any)</tspan></text>
<text x="410" y="534"><tspan fill="#58a6ff">ai</tspan> &rarr; board variants/ai <tspan class="cmt">(custom; not in core)</tspan></text>
</g>
</svg>
<div class="cap"><b>The whole model, on disk.</b> Canonical classes and shared variants live at the top, board-independent and PX4-owned. Each board dir mirrors that shape &mdash; <code>vehicles/</code> for class deltas, <code>variants/</code> for custom dev variants &mdash; holding only what the board diverges. A target name's five fields each resolve to a place in this tree: vendor/board to the board dir, runtime to a platform, class and variant to core-or-board by lookup. Green is shipped in #27667; the rest is this proposal. <i>(All staying Kconfig-native; folder names illustrative.)</i></div>
</div>
<div class="footer">
Conceptual model only. The detailed, file-level implementation analysis (current pipeline, the two-layer limit, the config-revert hazard, prototype sequence) lives in a separate companion document.
<br><br>
One honest caveat: the <span style="color:var(--red)">Zephyr</span> backend is entirely prospective. There is no Zephyr in the tree yet. Everything about NuttX, QuRT, Linux/POSIX, VOXL2, and the current model is observed and shipping today; only the Zephyr half is reasoned-from-analogy and needs a real spike to confirm.
<br><br>
Framing developed for the PX4 dev call. Thanks to <b style="color:var(--head)">Ramon Roche</b> and the PX4 maintainer community for the discussion that shaped it.
</div>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment