Skip to main content

consortium_data/
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//! Vendor device-tree sources, embedded at compile time and resolved from a chip
18//! part number plus an optional board selector.
19//!
20//! The builder needs the vendor board/SoC device tree to build the Linux device
21//! tree on top of. Those sources live under `database/dts/{imx9,stm32mp2}/` and
22//! are embedded here so the installed CLI can read them regardless of the working
23//! directory.
24
25use include_dir::{Dir, include_dir};
26
27use crate::error::DataError;
28
29/// The whole embedded `database/dts/` tree.
30static DTS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/database/dts");
31
32/// A base device tree selected for a chip.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct BaseDts {
35    /// Family subdirectory under `database/dts/` (e.g. `imx9`, `stm32mp2`). This
36    /// is the include-search root a downstream `cpp -I` needs so the base's
37    /// local `#include "..."` directives resolve.
38    pub family_dir: &'static str,
39    /// The entry file name, with extension (e.g. `stm32mp257f-dk.dts` or
40    /// `stm32mp257.dtsi`).
41    pub entry: String,
42}
43
44impl BaseDts {
45    /// Whether the entry is a full board `.dts` (as opposed to a generic
46    /// `.dtsi`). A `.dts` carries `/dts-v1/;` and board-level nodes; a `.dtsi`
47    /// is a SoC include with no board layer.
48    pub fn is_board(&self) -> bool {
49        self.entry.ends_with(".dts")
50    }
51
52    /// Path of the entry file within the embedded tree (e.g.
53    /// `imx9/imx95-15x15-frdm.dts`).
54    pub fn entry_path(&self) -> String {
55        format!("{}/{}", self.family_dir, self.entry)
56    }
57
58    /// The entry file's contents.
59    pub fn contents(&self) -> Result<&'static str, DataError> {
60        read(&self.entry_path())
61    }
62}
63
64/// Resolves the base device tree for a chip.
65///
66/// Selection order, highest priority first:
67/// 1. `base_override` (the `[dts].base` stem) — matched as `.dts` then `.dtsi`.
68/// 2. `devkit` (the `[profile].devkit` stem) — matched as `.dts` then `.dtsi`.
69/// 3. the generic SoC `.dtsi` derived from the chip part number.
70///
71/// # Errors
72///
73/// Returns [`DataError::UnknownChip`] if the chip family has no DTS directory,
74/// or [`DataError::UnknownDts`] if an explicit stem does not match any embedded
75/// file.
76pub fn resolve_base(
77    chip: &str,
78    devkit: Option<&str>,
79    base_override: Option<&str>,
80) -> Result<BaseDts, DataError> {
81    let family_dir = family_dir(chip).ok_or_else(|| DataError::UnknownChip(chip.to_string()))?;
82
83    if let Some(stem) = base_override.or(devkit) {
84        let entry =
85            match_stem(family_dir, stem).ok_or_else(|| DataError::UnknownDts(stem.to_string()))?;
86        return Ok(BaseDts { family_dir, entry });
87    }
88
89    let entry = generic_dtsi(chip).ok_or_else(|| DataError::UnknownChip(chip.to_string()))?;
90    // A generic mapping that does not resolve to an embedded file is a bug in the
91    // mapping table, so surface it as an unknown-DTS error rather than silently
92    // returning a dangling path.
93    if DTS.get_file(format!("{family_dir}/{entry}")).is_none() {
94        return Err(DataError::UnknownDts(entry));
95    }
96    Ok(BaseDts { family_dir, entry })
97}
98
99/// Reads a file from the embedded DTS tree by its tree-relative path (e.g.
100/// `imx9/imx95.dtsi`).
101pub fn read(path: &str) -> Result<&'static str, DataError> {
102    DTS.get_file(path)
103        .and_then(|f| f.contents_utf8())
104        .ok_or_else(|| DataError::UnknownDts(path.to_string()))
105}
106
107/// The transitive closure of a base file's *local* (`#include "..."`) includes
108/// within its family directory, as `(file_name, contents)` pairs.
109///
110/// The entry file itself is included first. Angled includes (`#include <...>`,
111/// i.e. `dt-bindings` headers) are not part of the embedded tree and are
112/// skipped; a downstream compile resolves them from the kernel tree. Missing
113/// local includes are skipped rather than erroring, since a base may reference an
114/// optional overlay-only include.
115pub fn local_include_closure(base: &BaseDts) -> Result<Vec<(String, String)>, DataError> {
116    let mut out: Vec<(String, String)> = Vec::new();
117    let mut seen: Vec<String> = Vec::new();
118    let mut queue: Vec<String> = vec![base.entry.clone()];
119
120    while let Some(name) = queue.pop() {
121        if seen.iter().any(|s| s == &name) {
122            continue;
123        }
124        seen.push(name.clone());
125        let path = format!("{}/{}", base.family_dir, name);
126        let Some(contents) = DTS.get_file(&path).and_then(|f| f.contents_utf8()) else {
127            // Not embedded (e.g. an angled dt-bindings header, or an optional
128            // include we don't ship): skip.
129            continue;
130        };
131        for included in local_includes(contents) {
132            queue.push(included);
133        }
134        out.push((name, contents.to_string()));
135    }
136
137    // Return in a stable, include-before-dependent-agnostic order: entry first as
138    // discovered, then the rest sorted for determinism.
139    Ok(out)
140}
141
142/// Resolves the base for a chip and returns its local include closure as
143/// `(file_name, contents)` pairs, ready to materialize next to a generated
144/// device tree so its quoted `#include`s resolve. A convenience wrapping
145/// [`resolve_base`] + [`local_include_closure`].
146pub fn base_closure(
147    chip: &str,
148    devkit: Option<&str>,
149    base_override: Option<&str>,
150) -> Result<Vec<(String, String)>, DataError> {
151    let base = resolve_base(chip, devkit, base_override)?;
152    local_include_closure(&base)
153}
154
155/// The `#include "..."` (quoted, local) file names referenced by `source`.
156fn local_includes(source: &str) -> Vec<String> {
157    let mut out = Vec::new();
158    for line in source.lines() {
159        let trimmed = line.trim_start();
160        let Some(rest) = trimmed.strip_prefix("#include") else {
161            continue;
162        };
163        let rest = rest.trim_start();
164        // Quoted, local include only. Angled `<...>` headers are not embedded.
165        if let Some(after) = rest.strip_prefix('"')
166            && let Some(end) = after.find('"')
167        {
168            out.push(after[..end].to_string());
169        }
170    }
171    out
172}
173
174/// The family subdirectory under `database/dts/` for a chip part number.
175fn family_dir(chip: &str) -> Option<&'static str> {
176    let needle = normalize(chip);
177    if needle.starts_with("stm32mp2") {
178        Some("stm32mp2")
179    } else if needle.starts_with("mimx9") {
180        Some("imx9")
181    } else {
182        None
183    }
184}
185
186/// Finds the embedded file matching a stem (no extension), preferring `.dts`
187/// (a board file) over `.dtsi` (a SoC include).
188fn match_stem(family_dir: &str, stem: &str) -> Option<String> {
189    for ext in ["dts", "dtsi"] {
190        let name = format!("{stem}.{ext}");
191        if DTS.get_file(format!("{family_dir}/{name}")).is_some() {
192            return Some(name);
193        }
194    }
195    None
196}
197
198/// The generic SoC `.dtsi` file name for a chip part number.
199///
200/// - i.MX: the `mimx9NN` prefix maps to `imx9N.dtsi` (e.g. `mimx9596` →
201///   `imx95.dtsi`).
202/// - STM32MP2: the leading `stm32mpNNN` of the part number is the SoC name
203///   (e.g. `stm32mp257fak3` → `stm32mp257.dtsi`).
204fn generic_dtsi(chip: &str) -> Option<String> {
205    let needle = normalize(chip);
206    if let Some(rest) = needle.strip_prefix("mimx9") {
207        // First digit after `mimx9` selects the SoC line (5 -> imx95, 3 -> imx93).
208        let line = rest.chars().next()?;
209        return Some(format!("imx9{line}.dtsi"));
210    }
211    if needle.starts_with("stm32mp") {
212        // `stm32mp` + the following three digits is the SoC dtsi stem.
213        let digits: String = needle
214            .chars()
215            .skip("stm32mp".len())
216            .take_while(|c| c.is_ascii_digit())
217            .collect();
218        if digits.len() >= 3 {
219            return Some(format!("stm32mp{}.dtsi", &digits[..3]));
220        }
221    }
222    None
223}
224
225/// Lowercases the part number and rewrites `imx` aliases to the `mimx` prefix,
226/// matching [`crate::database::load`]'s normalization.
227fn normalize(chip: &str) -> String {
228    let lowered = chip.to_ascii_lowercase();
229    match lowered.strip_prefix("imx") {
230        Some(rest) => format!("mimx{rest}"),
231        None => lowered,
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn devkit_stem_resolves_board_dts() {
241        let base = resolve_base("imx95", Some("imx95-15x15-frdm"), None).unwrap();
242        assert_eq!(base.family_dir, "imx9");
243        assert_eq!(base.entry, "imx95-15x15-frdm.dts");
244        assert!(base.is_board());
245        assert!(base.contents().unwrap().contains("/dts-v1/"));
246    }
247
248    #[test]
249    fn stm32_devkit_stem_resolves() {
250        let base = resolve_base("stm32mp257fak3", Some("stm32mp257f-dk"), None).unwrap();
251        assert_eq!(base.family_dir, "stm32mp2");
252        assert_eq!(base.entry, "stm32mp257f-dk.dts");
253    }
254
255    #[test]
256    fn no_devkit_falls_back_to_generic_dtsi() {
257        let imx = resolve_base("imx95", None, None).unwrap();
258        assert_eq!(imx.entry, "imx95.dtsi");
259        assert!(!imx.is_board());
260
261        let stm = resolve_base("stm32mp257fak3", None, None).unwrap();
262        assert_eq!(stm.entry, "stm32mp257.dtsi");
263    }
264
265    #[test]
266    fn base_override_wins_over_devkit() {
267        let base = resolve_base(
268            "stm32mp257fak3",
269            Some("stm32mp257f-dk"),
270            Some("stm32mp257f-ev1"),
271        )
272        .unwrap();
273        assert_eq!(base.entry, "stm32mp257f-ev1.dts");
274    }
275
276    #[test]
277    fn unknown_stem_errors() {
278        let err = resolve_base("imx95", Some("nope-not-a-board"), None).unwrap_err();
279        assert!(matches!(err, DataError::UnknownDts(_)));
280    }
281
282    #[test]
283    fn closure_includes_soc_dtsi_of_board() {
284        let base = resolve_base("stm32mp257fak3", Some("stm32mp257f-dk"), None).unwrap();
285        let closure = local_include_closure(&base).unwrap();
286        let names: Vec<&str> = closure.iter().map(|(n, _)| n.as_str()).collect();
287        assert!(names.contains(&"stm32mp257f-dk.dts"));
288        assert!(names.contains(&"stm32mp257.dtsi"));
289    }
290}