rlvgl 0.2.4

A modular, idiomatic Rust reimplementation of the LVGL graphics library for embedded and simulator use.
Documentation
//! lib.rs generator for rlvgl-creator.
//!
//! Scans BSP output directories and consolidates them into a `lib.rs`.

use std::{fs, path::Path};

use anyhow::{Context, Result};
use regex::Regex;

/// Available forms discovered for a board.
#[derive(Clone, Copy, Default)]
pub(crate) struct Forms {
    /// HAL split form is present.
    pub hal_split: bool,
    /// HAL flat form is present.
    pub hal_flat: bool,
    /// PAC split form is present.
    pub pac_split: bool,
    /// PAC flat form is present.
    pub pac_flat: bool,
    /// Summary file is present.
    pub summary: bool,
    /// Pin report is present.
    pub pinreport: bool,
}

fn slugify(name: &str) -> String {
    let mut s = name.to_lowercase();
    s = s.replace(['(', ')', '[', ']', '-', ' ', '.'], "_");
    let re = Regex::new(r"[^a-z0-9_]").unwrap();
    let s = re.replace_all(&s, "");
    let re2 = Regex::new(r"_+").unwrap();
    re2.replace_all(&s, "_").trim_matches('_').to_string()
}

fn family_from_slug(slug: &str) -> Option<String> {
    let re = Regex::new(r"^stm32([a-z])(\d)").unwrap();
    re.captures(slug)
        .map(|c| format!("stm32-{}{}", &c[1], &c[2]))
}

/// Walks `src` discovering available forms for each board.
pub(crate) fn scan_tree(src: &Path) -> Result<Vec<(String, Forms)>> {
    let mut out = vec![];
    for entry in fs::read_dir(src).with_context(|| format!("reading {}", src.display()))? {
        let entry = entry?;
        if !entry.file_type()?.is_dir() {
            continue;
        }
        let mcu_dir = entry.path();
        let slug = slugify(entry.file_name().to_string_lossy().as_ref());
        let mut f = Forms::default();
        let has = |p: &Path| p.exists();
        if has(&mcu_dir.join("hal/split.rs")) {
            f.hal_split = true;
        }
        if has(&mcu_dir.join("hal/flat.rs")) {
            f.hal_flat = true;
        }
        if has(&mcu_dir.join("pac/split.rs")) {
            f.pac_split = true;
        }
        if has(&mcu_dir.join("pac/flat.rs")) {
            f.pac_flat = true;
        }
        if has(&mcu_dir.join("summary.rs")) {
            f.summary = true;
        }
        if has(&mcu_dir.join("pinreport.rs")) {
            f.pinreport = true;
        }
        out.push((slug, f));
    }
    out.sort_by(|a, b| a.0.cmp(&b.0));
    Ok(out)
}

/// Emit a consolidated `lib.rs` from BSP fragments.
pub(crate) fn emit_lib_rs(
    src: impl AsRef<Path>,
    out: impl AsRef<Path>,
    prelude: Option<(&str, &str)>,
    features: &[String],
    family_prefix: Option<&str>,
    inline_includes: bool,
) -> Result<()> {
    let src = src.as_ref();
    let mcus = scan_tree(src)?;
    let mut s = String::new();

    s.push_str("//! Auto-generated by rlvgl-creator — DO NOT EDIT\n#![no_std]\n\n");

    if let Some(default_form) = prelude {
        s.push_str("/// Ergonomic re-exports of the default form.\npub mod prelude {\n");
        for (slug, forms) in &mcus {
            match default_form {
                ("hal", "split")
                    if forms.hal_split
                        && features.contains(&"hal".to_string())
                        && features.contains(&"split".to_string()) =>
                {
                    s.push_str(&format!(
                        "    #[cfg(all(feature=\"hal\", feature=\"split\"))]\n    pub mod {slug} {{ pub use crate::{slug}::hal::split::*; }}\n",
                    ));
                }
                ("pac", "split")
                    if forms.pac_split
                        && features.contains(&"pac".to_string())
                        && features.contains(&"split".to_string()) =>
                {
                    s.push_str(&format!(
                        "    #[cfg(all(feature=\"pac\", feature=\"split\"))]\n    pub mod {slug} {{ pub use crate::{slug}::pac::split::*; }}\n",
                    ));
                }
                ("hal", "flat")
                    if forms.hal_flat
                        && features.contains(&"hal".to_string())
                        && features.contains(&"flat".to_string()) =>
                {
                    s.push_str(&format!(
                        "    #[cfg(all(feature=\"hal\", feature=\"flat\"))]\n    pub mod {slug} {{ pub use crate::{slug}::hal::flat::*; }}\n",
                    ));
                }
                ("pac", "flat")
                    if forms.pac_flat
                        && features.contains(&"pac".to_string())
                        && features.contains(&"flat".to_string()) =>
                {
                    s.push_str(&format!(
                        "    #[cfg(all(feature=\"pac\", feature=\"flat\"))]\n    pub mod {slug} {{ pub use crate::{slug}::pac::flat::*; }}\n",
                    ));
                }
                _ => {}
            }
        }
        s.push_str("}\n\n");
    }

    for (slug, forms) in mcus {
        if !inline_includes {
            if let (Some(prefix), Some(fam)) = (family_prefix, family_from_slug(&slug)) {
                let fam = fam.strip_prefix("stm32-").unwrap_or(&fam);
                s.push_str(&format!("#[cfg(feature=\"{}{}\")]\n", prefix, fam));
            }
            s.push_str(&format!("pub mod {slug};\n\n"));
            continue;
        }

        if let (Some(prefix), Some(fam)) = (family_prefix, family_from_slug(&slug)) {
            let fam = fam.strip_prefix("stm32-").unwrap_or(&fam);
            s.push_str(&format!("#[cfg(feature=\"{}{}\")]\n", prefix, fam));
        }
        s.push_str(&format!("pub mod {slug} {{\n"));
        if (forms.hal_split || forms.hal_flat) && features.contains(&"hal".to_string()) {
            s.push_str("    #[cfg(feature=\"hal\")] pub mod hal {\n");
            if forms.hal_split && features.contains(&"split".to_string()) {
                if inline_includes {
                    s.push_str(&format!(
                        "        #[cfg(feature=\"split\")] pub mod split {{ include!(\"{slug}/hal/split.rs\"); }}\n",
                    ));
                } else {
                    s.push_str("        #[cfg(feature=\"split\")] pub mod split;\n");
                }
            }
            if forms.hal_flat && features.contains(&"flat".to_string()) {
                if inline_includes {
                    s.push_str(&format!(
                        "        #[cfg(feature=\"flat\")] pub mod flat {{ include!(\"{slug}/hal/flat.rs\"); }}\n",
                    ));
                } else {
                    s.push_str("        #[cfg(feature=\"flat\")] pub mod flat;\n");
                }
            }
            s.push_str("    }\n");
        }
        if (forms.pac_split || forms.pac_flat) && features.contains(&"pac".to_string()) {
            s.push_str("    #[cfg(feature=\"pac\")] pub mod pac {\n");
            if forms.pac_split && features.contains(&"split".to_string()) {
                if inline_includes {
                    s.push_str(&format!(
                        "        #[cfg(feature=\"split\")] pub mod split {{ include!(\"{slug}/pac/split.rs\"); }}\n",
                    ));
                } else {
                    s.push_str("        #[cfg(feature=\"split\")] pub mod split;\n");
                }
            }
            if forms.pac_flat && features.contains(&"flat".to_string()) {
                if inline_includes {
                    s.push_str(&format!(
                        "        #[cfg(feature=\"flat\")] pub mod flat {{ include!(\"{slug}/pac/flat.rs\"); }}\n",
                    ));
                } else {
                    s.push_str("        #[cfg(feature=\"flat\")] pub mod flat;\n");
                }
            }
            s.push_str("    }\n");
        }
        if forms.summary && features.contains(&"summaries".to_string()) {
            if inline_includes {
                s.push_str(&format!(
                    "    #[cfg(feature=\"summaries\")] pub mod summary {{ include!(\"{slug}/summary.rs\"); }}\n",
                ));
            } else {
                s.push_str("    #[cfg(feature=\"summaries\")] pub mod summary;\n");
            }
        }
        if forms.pinreport && features.contains(&"pinreport".to_string()) {
            if inline_includes {
                s.push_str(&format!(
                    "    #[cfg(feature=\"pinreport\")] pub mod pinreport {{ include!(\"{slug}/pinreport.rs\"); }}\n",
                ));
            } else {
                s.push_str("    #[cfg(feature=\"pinreport\")] pub mod pinreport;\n");
            }
        }
        s.push_str("}\n\n");
    }

    fs::write(out, s).context("writing lib.rs")
}