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;
28
29pub use consortium_cfg::components::dts_render::{RenderedDts, render_linux_dts};
30
31/// File name of the generated kernel config fragment under `dist/kernel/`.
32pub const KCONFIG_NAME: &str = "consortium.cfg";
33
34/// Renders the single kernel config fragment for every generated Linux device.
35///
36/// UIO devices enable the userspace-I/O generic-IRQ drivers their device-tree
37/// nodes bind against:
38///
39/// - `CONFIG_UIO` — the UIO core;
40/// - `CONFIG_UIO_PDRV_GENIRQ` — the platform driver matched by `generic-uio`;
41/// - `CONFIG_UIO_DMEM_GENIRQ` — its dynamic-memory variant for RAM carveouts.
42///
43/// Remoteproc devices enable the core framework and the selected SoC driver.
44///
45/// Every fragment also enables `CONFIG_OF_OVERLAY` and `CONFIG_OF_CONFIGFS`, in
46/// both DTS modes, so overlays can be applied at runtime through configfs.
47///
48/// The resulting `consortium.cfg` is consumed by the integrator's kernel recipe,
49/// not installed into the rootfs. Returns `None` when neither class is present.
50pub fn render_kernel_kconfig(chip: &str, ir: &LoweredIr) -> Option<String> {
51    let mut out = String::new();
52    if !ir.uio.is_empty() {
53        out.push_str(
54            "CONFIG_UIO=y\n\
55             CONFIG_UIO_PDRV_GENIRQ=y\n\
56             CONFIG_UIO_DMEM_GENIRQ=y\n",
57        );
58    }
59    if !ir.remoteproc.is_empty() {
60        let driver = if chip.starts_with("mimx9") {
61            Some("CONFIG_IMX_REMOTEPROC=y\n")
62        } else if chip.starts_with("stm32mp2") {
63            Some("CONFIG_STM32_RPROC=y\n")
64        } else {
65            None
66        };
67        if let Some(driver) = driver {
68            out.push_str("CONFIG_REMOTEPROC=y\n");
69            out.push_str(driver);
70        }
71    }
72    if out.is_empty() {
73        return None;
74    }
75    out.push_str(
76        "CONFIG_OF_OVERLAY=y\n\
77         CONFIG_OF_CONFIGFS=y\n",
78    );
79    Some(out)
80}
81
82/// The `.bbappend` file name for `kernel_recipe`, version-wildcarded so it
83/// applies to whatever kernel version the BSP provides (e.g.
84/// `linux-stm32mp_%.bbappend`).
85pub fn kernel_bbappend_name(kernel_recipe: &str) -> String {
86    format!("{kernel_recipe}_%.bbappend")
87}
88
89/// Renders a kernel `.bbappend` that merges `fragments` into the kernel config
90/// at build time (via `KERNEL_CONFIG_FRAGMENTS`), instead of shipping them in the
91/// rootfs.
92///
93/// The `.cfg` files sit next to the `.bbappend` (both under `dist/kernel/`), so
94/// `FILESEXTRAPATHS:prepend := "${THISDIR}:"` lets bitbake resolve the
95/// `file://<name>` fetches. Returns `None` when there are no fragments.
96pub fn render_kernel_bbappend(fragments: &[String]) -> Option<String> {
97    if fragments.is_empty() {
98        return None;
99    }
100
101    let src_uri = fragments
102        .iter()
103        .map(|name| format!("    file://{name} \\"))
104        .collect::<Vec<_>>()
105        .join("\n");
106    let merged = fragments
107        .iter()
108        .map(|name| format!("    ${{WORKDIR}}/{name} \\"))
109        .collect::<Vec<_>>()
110        .join("\n");
111
112    Some(format!(
113        "# Auto-generated by Consortium — do not edit by hand.\n\
114         #\n\
115         # Merges the UIO kernel config fragment(s) into the kernel build. Drop this\n\
116         # file and the sibling `.cfg`(s) into your BSP's kernel recipe directory\n\
117         # (e.g. recipes-kernel/linux/).\n\
118         \n\
119         FILESEXTRAPATHS:prepend := \"${{THISDIR}}:\"\n\
120         \n\
121         SRC_URI += \"\\\n{src_uri}\n\"\n\
122         \n\
123         KERNEL_CONFIG_FRAGMENTS:append = \"\\\n{merged}\n\"\n"
124    ))
125}
126
127/// The kernel command-line argument the `generic-uio` platform driver needs so
128/// it binds the `generic-uio`-compatible nodes.
129const UIO_BOOTARG: &str = "uio_pdrv_genirq.of_id=generic-uio";
130
131/// Renders the boot-args artifacts under `dist/boot/` for the chip family, or
132/// `None` when the manifest declares no UIO devices (no `generic-uio` nodes to
133/// bind, so no boot arg is needed).
134///
135/// The kernel command line cannot be edited live from the pipeline, so these are
136/// files the integrator wires into their build:
137///
138/// - **STM32 / OpenSTLinux** (extlinux boot): an `extlinux-uio.cfg` fragment
139///   appending `UIO_BOOTARG` through `UBOOT_EXTLINUX_KERNEL_ARGS`, which
140///   regenerates `extlinux.conf` persistently at build time.
141/// - **NXP i.MX** (scripted boot, no extlinux): a `boot.cmd` U-Boot script
142///   source to compile to `boot.scr` (via `mkimage`) and stage on the FAT boot
143///   partition through `IMAGE_BOOT_FILES`.
144///
145/// Each family also gets a `README` describing the exact integration steps.
146pub fn render_boot(chip: &str, ir: &LoweredIr) -> Vec<(String, String)> {
147    if ir.uio.is_empty() {
148        return Vec::new();
149    }
150
151    if is_stm32(chip) {
152        vec![
153            (
154                "extlinux-uio.cfg".to_string(),
155                format!(
156                    "# Auto-generated by Consortium — append the UIO kernel arg to the\n\
157                     # OpenSTLinux extlinux command line. Add to a u-boot bbappend or a\n\
158                     # machine/distro .conf so `extlinux.conf` is regenerated with it.\n\
159                     UBOOT_EXTLINUX_KERNEL_ARGS:append = \" {UIO_BOOTARG}\"\n"
160                ),
161            ),
162            (
163                "README".to_string(),
164                format!(
165                    "Consortium boot-args (STM32 / OpenSTLinux)\n\
166                     =========================================\n\n\
167                     The `generic-uio` driver only binds the Consortium UIO nodes when the\n\
168                     kernel command line carries:\n\n    \
169                     {UIO_BOOTARG}\n\n\
170                     OpenSTLinux boots via extlinux, so this is a build-time change:\n\n\
171                     1. Copy `extlinux-uio.cfg` into your u-boot recipe (or a machine/distro\n\
172                        `.conf`) so its `UBOOT_EXTLINUX_KERNEL_ARGS:append` takes effect.\n\
173                     2. Rebuild; the generated `/boot/mmcN_extlinux/extlinux.conf` `APPEND`\n\
174                        line now ends with the UIO arg.\n\n\
175                     No manual `setenv` at the U-Boot prompt is required.\n"
176                ),
177            ),
178        ]
179    } else {
180        vec![
181            (
182                "boot.cmd".to_string(),
183                format!(
184                    "# Auto-generated by Consortium — U-Boot boot script source.\n\
185                     # Appends the UIO kernel arg to the board's bootargs, then boots as usual.\n\
186                     # Compile and stage it (no U-Boot rebuild needed):\n\
187                     #   mkimage -A arm64 -O linux -T script -C none -d boot.cmd boot.scr\n\
188                     # then add `boot.scr` to IMAGE_BOOT_FILES so wic writes it to the FAT\n\
189                     # boot partition; the i.MX distro bootcmd loads and runs it.\n\
190                     setenv bootargs \"${{bootargs}} {UIO_BOOTARG}\"\n\
191                     run distro_bootcmd\n"
192                ),
193            ),
194            (
195                "README".to_string(),
196                format!(
197                    "Consortium boot-args (NXP i.MX)\n\
198                     ===============================\n\n\
199                     The `generic-uio` driver only binds the Consortium UIO nodes when the\n\
200                     kernel command line carries:\n\n    \
201                     {UIO_BOOTARG}\n\n\
202                     i.MX U-Boot uses a scripted boot (no extlinux.conf), so ship a boot\n\
203                     script rather than editing a `.conf`:\n\n\
204                     1. Compile the script:\n\
205                        mkimage -A arm64 -O linux -T script -C none -d boot.cmd boot.scr\n\
206                     2. Stage `boot.scr` on the FAT boot partition, e.g. in your image recipe:\n\
207                        IMAGE_BOOT_FILES += \"boot.scr\"\n\
208                     3. Boot; U-Boot's distro bootcmd loads `boot.scr`, which appends the UIO\n\
209                        arg to `bootargs` before booting.\n\n\
210                     Alternative (baked into U-Boot): append the arg to `mmcargs` /\n\
211                     `CONFIG_EXTRA_ENV_SETTINGS` via a `u-boot_%.bbappend`.\n"
212                ),
213            ),
214        ]
215    }
216}
217
218/// Whether the chip is an STM32 part (ST / OpenSTLinux flow); otherwise it is
219/// treated as NXP i.MX. Mirrors the family split in
220/// `consortium_cfg::bridge::hal_crate_path`.
221fn is_stm32(chip: &str) -> bool {
222    chip.to_ascii_lowercase().starts_with("stm32")
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    fn ir_with_uio() -> LoweredIr {
230        use consortium_cfg::components::uio::{UioBacking, UioDevice};
231        LoweredIr::empty_with_uio(vec![(
232            "ipc-shm".to_string(),
233            UioDevice {
234                mmap: vec![(0x2040_0000, 0x8000)],
235                irq: None,
236                backing: UioBacking::ReservedMemory,
237                dma_coherent: Vec::new(),
238                interrupt_parent: None,
239                clock: None,
240            },
241        )])
242    }
243
244    #[test]
245    fn kconfig_enables_genirq_and_overlay_configfs() {
246        let cfg = render_kernel_kconfig("mimx9596xvt", &ir_with_uio()).unwrap();
247        assert!(cfg.contains("CONFIG_UIO=y"));
248        assert!(cfg.contains("CONFIG_UIO_PDRV_GENIRQ=y"));
249        assert!(cfg.contains("CONFIG_UIO_DMEM_GENIRQ=y"));
250        assert!(!cfg.contains("GENERIC"));
251        assert!(cfg.contains("CONFIG_OF_OVERLAY=y"));
252        assert!(cfg.contains("CONFIG_OF_CONFIGFS=y"));
253    }
254
255    #[test]
256    fn kconfig_none_without_uio() {
257        let empty = LoweredIr::empty_with_uio(vec![]);
258        assert!(render_kernel_kconfig("mimx9596xvt", &empty).is_none());
259    }
260
261    #[test]
262    fn remoteproc_kconfig_tracks_soc_family_without_uio() {
263        let ir = consortium_cfg::Session::from_toml(
264            r#"
265                [profile]
266                chip = "mimx9596xvt"
267                shared = "./shared"
268                deploy_root = "/opt/consortium"
269
270                [endpoints]
271                cm7 = "./mcu"
272            "#,
273        )
274        .unwrap()
275        .lower()
276        .unwrap();
277        let cfg = render_kernel_kconfig("mimx9596xvt", &ir).unwrap();
278        assert_eq!(
279            cfg,
280            "CONFIG_REMOTEPROC=y\nCONFIG_IMX_REMOTEPROC=y\nCONFIG_OF_OVERLAY=y\nCONFIG_OF_CONFIGFS=y\n"
281        );
282    }
283
284    #[test]
285    fn boot_stm32_emits_extlinux_fragment() {
286        let files = render_boot("stm32mp257fak3", &ir_with_uio());
287        let names: Vec<&str> = files.iter().map(|(n, _)| n.as_str()).collect();
288        assert!(names.contains(&"extlinux-uio.cfg"));
289        assert!(names.contains(&"README"));
290        let cfg = &files
291            .iter()
292            .find(|(n, _)| n == "extlinux-uio.cfg")
293            .unwrap()
294            .1;
295        assert!(cfg.contains("UBOOT_EXTLINUX_KERNEL_ARGS:append"));
296        assert!(cfg.contains(UIO_BOOTARG));
297    }
298
299    #[test]
300    fn boot_nxp_emits_boot_cmd() {
301        let files = render_boot("imx95", &ir_with_uio());
302        let names: Vec<&str> = files.iter().map(|(n, _)| n.as_str()).collect();
303        assert!(names.contains(&"boot.cmd"));
304        let cmd = &files.iter().find(|(n, _)| n == "boot.cmd").unwrap().1;
305        assert!(cmd.contains("setenv bootargs"));
306        assert!(cmd.contains(UIO_BOOTARG));
307        assert!(cmd.contains("mkimage"));
308    }
309
310    #[test]
311    fn boot_none_without_uio() {
312        assert!(render_boot("imx95", &LoweredIr::empty_with_uio(vec![])).is_empty());
313    }
314
315    #[test]
316    fn bbappend_name_wildcards_version() {
317        assert_eq!(
318            kernel_bbappend_name("linux-stm32mp"),
319            "linux-stm32mp_%.bbappend"
320        );
321    }
322
323    #[test]
324    fn bbappend_merges_fragments_at_build_time() {
325        let bb = render_kernel_bbappend(&[KCONFIG_NAME.to_string()])
326            .expect("fragment present yields a bbappend");
327        assert!(bb.contains("FILESEXTRAPATHS:prepend := \"${THISDIR}:\""));
328        assert!(bb.contains("file://consortium.cfg"));
329        assert!(bb.contains("KERNEL_CONFIG_FRAGMENTS:append"));
330        assert!(bb.contains("${WORKDIR}/consortium.cfg"));
331        // It is a kernel build input, so it must not touch the rootfs.
332        assert!(!bb.contains("do_install"));
333        assert!(!bb.contains("${D}"));
334    }
335
336    #[test]
337    fn bbappend_none_without_fragments() {
338        assert!(render_kernel_bbappend(&[]).is_none());
339    }
340}