Skip to main content

consortium_builder/pipeline/
dts.rs

1// Copyright 2026 Ethan Wu
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Renders the Linux-side kernel-config and boot artifacts from the lowered IR.
18//!
19//! The device-tree text itself (merged `.dts` or `.dtso` overlay) is rendered by
20//! [`consortium_cfg::components::dts_render`], which owns the vendor-base
21//! resolution and parsing. This module renders the plainer text artifacts that
22//! accompany it — the kernel config fragment enabling the UIO drivers, the
23//! kernel `.bbappend` that merges it, and the boot-args files that put
24//! `uio_pdrv_genirq.of_id=generic-uio` on the kernel command line — and stays
25//! free of filesystem and toolchain concerns (that is [`super::dist`]'s job).
26
27use consortium_cfg::bridge::LoweredIr;
28use consortium_cfg_common::config::DtsMode;
29
30pub use consortium_cfg::components::dts_render::{RenderedDts, render_linux_dts};
31
32/// File name of the generated kernel config fragment under `dist/kernel/`.
33pub const UIO_KCONFIG_NAME: &str = "uio-enable.cfg";
34
35/// Renders the kernel config fragment that enables the drivers the UIO device
36/// tree relies on, or `None` when the manifest declares no UIO devices.
37///
38/// Always enables the userspace-I/O generic-IRQ drivers the `generic-uio` nodes
39/// bind against:
40///
41/// - `CONFIG_UIO` — the UIO core;
42/// - `CONFIG_UIO_PDRV_GENIRQ` — the platform driver matched by `generic-uio`;
43/// - `CONFIG_UIO_DMEM_GENIRQ` — its dynamic-memory variant for RAM carveouts.
44///
45/// In [`DtsMode::Overlay`] it additionally enables `CONFIG_OF_OVERLAY` (needed
46/// to apply the overlay) and, when `configfs` is set, `CONFIG_OF_CONFIGFS` (to
47/// apply it at runtime through configfs). A merged `.dts` needs neither.
48///
49/// The fragment is consumed by the integrator's kernel recipe (a
50/// `SRC_URI += "file://..."` merge), not installed into the rootfs.
51pub fn render_uio_kconfig(ir: &LoweredIr, mode: DtsMode, configfs: bool) -> Option<String> {
52    if ir.uio.is_empty() {
53        return None;
54    }
55
56    let mut out = String::from(
57        "CONFIG_UIO=y\n\
58         CONFIG_UIO_PDRV_GENIRQ=y\n\
59         CONFIG_UIO_DMEM_GENIRQ=y\n",
60    );
61    if mode.is_overlay() {
62        out.push_str("CONFIG_OF_OVERLAY=y\n");
63        if configfs {
64            out.push_str("CONFIG_OF_CONFIGFS=y\n");
65        }
66    }
67    Some(out)
68}
69
70/// The `.bbappend` file name for `kernel_recipe`, version-wildcarded so it
71/// applies to whatever kernel version the BSP provides (e.g.
72/// `linux-stm32mp_%.bbappend`).
73pub fn kernel_bbappend_name(kernel_recipe: &str) -> String {
74    format!("{kernel_recipe}_%.bbappend")
75}
76
77/// Renders a kernel `.bbappend` that merges `fragments` into the kernel config
78/// at build time (via `KERNEL_CONFIG_FRAGMENTS`), instead of shipping them in the
79/// rootfs.
80///
81/// The `.cfg` files sit next to the `.bbappend` (both under `dist/kernel/`), so
82/// `FILESEXTRAPATHS:prepend := "${THISDIR}:"` lets bitbake resolve the
83/// `file://<name>` fetches. Returns `None` when there are no fragments.
84pub fn render_kernel_bbappend(fragments: &[String]) -> Option<String> {
85    if fragments.is_empty() {
86        return None;
87    }
88
89    let src_uri = fragments
90        .iter()
91        .map(|name| format!("    file://{name} \\"))
92        .collect::<Vec<_>>()
93        .join("\n");
94    let merged = fragments
95        .iter()
96        .map(|name| format!("    ${{WORKDIR}}/{name} \\"))
97        .collect::<Vec<_>>()
98        .join("\n");
99
100    Some(format!(
101        "# Auto-generated by Consortium — do not edit by hand.\n\
102         #\n\
103         # Merges the UIO kernel config fragment(s) into the kernel build. Drop this\n\
104         # file and the sibling `.cfg`(s) into your BSP's kernel recipe directory\n\
105         # (e.g. recipes-kernel/linux/).\n\
106         \n\
107         FILESEXTRAPATHS:prepend := \"${{THISDIR}}:\"\n\
108         \n\
109         SRC_URI += \"\\\n{src_uri}\n\"\n\
110         \n\
111         KERNEL_CONFIG_FRAGMENTS:append = \"\\\n{merged}\n\"\n"
112    ))
113}
114
115/// The kernel command-line argument the `generic-uio` platform driver needs so
116/// it binds the `generic-uio`-compatible nodes.
117const UIO_BOOTARG: &str = "uio_pdrv_genirq.of_id=generic-uio";
118
119/// Renders the boot-args artifacts under `dist/boot/` for the chip family, or
120/// `None` when the manifest declares no UIO devices (no `generic-uio` nodes to
121/// bind, so no boot arg is needed).
122///
123/// The kernel command line cannot be edited live from the pipeline, so these are
124/// files the integrator wires into their build:
125///
126/// - **STM32 / OpenSTLinux** (extlinux boot): an `extlinux-uio.cfg` fragment
127///   appending [`UIO_BOOTARG`] through `UBOOT_EXTLINUX_KERNEL_ARGS`, which
128///   regenerates `extlinux.conf` persistently at build time.
129/// - **NXP i.MX** (scripted boot, no extlinux): a `boot.cmd` U-Boot script
130///   source to compile to `boot.scr` (via `mkimage`) and stage on the FAT boot
131///   partition through `IMAGE_BOOT_FILES`.
132///
133/// Each family also gets a `README` describing the exact integration steps.
134pub fn render_boot(chip: &str, ir: &LoweredIr) -> Vec<(String, String)> {
135    if ir.uio.is_empty() {
136        return Vec::new();
137    }
138
139    if is_stm32(chip) {
140        vec![
141            (
142                "extlinux-uio.cfg".to_string(),
143                format!(
144                    "# Auto-generated by Consortium — append the UIO kernel arg to the\n\
145                     # OpenSTLinux extlinux command line. Add to a u-boot bbappend or a\n\
146                     # machine/distro .conf so `extlinux.conf` is regenerated with it.\n\
147                     UBOOT_EXTLINUX_KERNEL_ARGS:append = \" {UIO_BOOTARG}\"\n"
148                ),
149            ),
150            (
151                "README".to_string(),
152                format!(
153                    "Consortium boot-args (STM32 / OpenSTLinux)\n\
154                     =========================================\n\n\
155                     The `generic-uio` driver only binds the Consortium UIO nodes when the\n\
156                     kernel command line carries:\n\n    \
157                     {UIO_BOOTARG}\n\n\
158                     OpenSTLinux boots via extlinux, so this is a build-time change:\n\n\
159                     1. Copy `extlinux-uio.cfg` into your u-boot recipe (or a machine/distro\n\
160                        `.conf`) so its `UBOOT_EXTLINUX_KERNEL_ARGS:append` takes effect.\n\
161                     2. Rebuild; the generated `/boot/mmcN_extlinux/extlinux.conf` `APPEND`\n\
162                        line now ends with the UIO arg.\n\n\
163                     No manual `setenv` at the U-Boot prompt is required.\n"
164                ),
165            ),
166        ]
167    } else {
168        vec![
169            (
170                "boot.cmd".to_string(),
171                format!(
172                    "# Auto-generated by Consortium — U-Boot boot script source.\n\
173                     # Appends the UIO kernel arg to the board's bootargs, then boots as usual.\n\
174                     # Compile and stage it (no U-Boot rebuild needed):\n\
175                     #   mkimage -A arm64 -O linux -T script -C none -d boot.cmd boot.scr\n\
176                     # then add `boot.scr` to IMAGE_BOOT_FILES so wic writes it to the FAT\n\
177                     # boot partition; the i.MX distro bootcmd loads and runs it.\n\
178                     setenv bootargs \"${{bootargs}} {UIO_BOOTARG}\"\n\
179                     run distro_bootcmd\n"
180                ),
181            ),
182            (
183                "README".to_string(),
184                format!(
185                    "Consortium boot-args (NXP i.MX)\n\
186                     ===============================\n\n\
187                     The `generic-uio` driver only binds the Consortium UIO nodes when the\n\
188                     kernel command line carries:\n\n    \
189                     {UIO_BOOTARG}\n\n\
190                     i.MX U-Boot uses a scripted boot (no extlinux.conf), so ship a boot\n\
191                     script rather than editing a `.conf`:\n\n\
192                     1. Compile the script:\n\
193                        mkimage -A arm64 -O linux -T script -C none -d boot.cmd boot.scr\n\
194                     2. Stage `boot.scr` on the FAT boot partition, e.g. in your image recipe:\n\
195                        IMAGE_BOOT_FILES += \"boot.scr\"\n\
196                     3. Boot; U-Boot's distro bootcmd loads `boot.scr`, which appends the UIO\n\
197                        arg to `bootargs` before booting.\n\n\
198                     Alternative (baked into U-Boot): append the arg to `mmcargs` /\n\
199                     `CONFIG_EXTRA_ENV_SETTINGS` via a `u-boot_%.bbappend`.\n"
200                ),
201            ),
202        ]
203    }
204}
205
206/// Whether the chip is an STM32 part (ST / OpenSTLinux flow); otherwise it is
207/// treated as NXP i.MX. Mirrors the family split in
208/// `consortium_cfg::bridge::hal_crate_path`.
209fn is_stm32(chip: &str) -> bool {
210    chip.to_ascii_lowercase().starts_with("stm32")
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    fn ir_with_uio() -> LoweredIr {
218        use consortium_cfg::components::uio::{UioBacking, UioDevice};
219        LoweredIr::empty_with_uio(vec![(
220            "ipc-shm".to_string(),
221            UioDevice {
222                mmap: vec![(0x2040_0000, 0x8000)],
223                irq: None,
224                backing: UioBacking::ReservedMemory,
225                interrupt_parent: None,
226                clock: None,
227            },
228        )])
229    }
230
231    #[test]
232    fn kconfig_merge_mode_uses_genirq_symbols_without_overlay() {
233        let cfg = render_uio_kconfig(&ir_with_uio(), DtsMode::Merge, false).unwrap();
234        assert!(cfg.contains("CONFIG_UIO=y"));
235        assert!(cfg.contains("CONFIG_UIO_PDRV_GENIRQ=y"));
236        assert!(cfg.contains("CONFIG_UIO_DMEM_GENIRQ=y"));
237        // No stale GENERIC symbols, and no overlay config in merge mode.
238        assert!(!cfg.contains("GENERIC"));
239        assert!(!cfg.contains("CONFIG_OF_OVERLAY"));
240        assert!(!cfg.contains("CONFIG_OF_CONFIGFS"));
241    }
242
243    #[test]
244    fn kconfig_overlay_mode_enables_of_overlay_and_optional_configfs() {
245        let no_cfgfs = render_uio_kconfig(&ir_with_uio(), DtsMode::Overlay, false).unwrap();
246        assert!(no_cfgfs.contains("CONFIG_OF_OVERLAY=y"));
247        assert!(!no_cfgfs.contains("CONFIG_OF_CONFIGFS"));
248
249        let with_cfgfs = render_uio_kconfig(&ir_with_uio(), DtsMode::Overlay, true).unwrap();
250        assert!(with_cfgfs.contains("CONFIG_OF_CONFIGFS=y"));
251    }
252
253    #[test]
254    fn kconfig_none_without_uio() {
255        let empty = LoweredIr::empty_with_uio(vec![]);
256        assert!(render_uio_kconfig(&empty, DtsMode::Merge, false).is_none());
257    }
258
259    #[test]
260    fn boot_stm32_emits_extlinux_fragment() {
261        let files = render_boot("stm32mp257fak3", &ir_with_uio());
262        let names: Vec<&str> = files.iter().map(|(n, _)| n.as_str()).collect();
263        assert!(names.contains(&"extlinux-uio.cfg"));
264        assert!(names.contains(&"README"));
265        let cfg = &files
266            .iter()
267            .find(|(n, _)| n == "extlinux-uio.cfg")
268            .unwrap()
269            .1;
270        assert!(cfg.contains("UBOOT_EXTLINUX_KERNEL_ARGS:append"));
271        assert!(cfg.contains(UIO_BOOTARG));
272    }
273
274    #[test]
275    fn boot_nxp_emits_boot_cmd() {
276        let files = render_boot("imx95", &ir_with_uio());
277        let names: Vec<&str> = files.iter().map(|(n, _)| n.as_str()).collect();
278        assert!(names.contains(&"boot.cmd"));
279        let cmd = &files.iter().find(|(n, _)| n == "boot.cmd").unwrap().1;
280        assert!(cmd.contains("setenv bootargs"));
281        assert!(cmd.contains(UIO_BOOTARG));
282        assert!(cmd.contains("mkimage"));
283    }
284
285    #[test]
286    fn boot_none_without_uio() {
287        assert!(render_boot("imx95", &LoweredIr::empty_with_uio(vec![])).is_empty());
288    }
289
290    #[test]
291    fn bbappend_name_wildcards_version() {
292        assert_eq!(
293            kernel_bbappend_name("linux-stm32mp"),
294            "linux-stm32mp_%.bbappend"
295        );
296    }
297
298    #[test]
299    fn bbappend_merges_fragments_at_build_time() {
300        let bb = render_kernel_bbappend(&["uio-enable.cfg".to_string()])
301            .expect("fragment present yields a bbappend");
302        assert!(bb.contains("FILESEXTRAPATHS:prepend := \"${THISDIR}:\""));
303        assert!(bb.contains("file://uio-enable.cfg"));
304        assert!(bb.contains("KERNEL_CONFIG_FRAGMENTS:append"));
305        assert!(bb.contains("${WORKDIR}/uio-enable.cfg"));
306        // It is a kernel build input, so it must not touch the rootfs.
307        assert!(!bb.contains("do_install"));
308        assert!(!bb.contains("${D}"));
309    }
310
311    #[test]
312    fn bbappend_none_without_fragments() {
313        assert!(render_kernel_bbappend(&[]).is_none());
314    }
315}