rlvgl 0.2.4

A modular, idiomatic Rust reimplementation of the LVGL graphics library for embedded and simulator use.
Documentation
//! Scaffold command for rlvgl-creator.
//!
//! Generates a dual-mode assets crate using embedded MiniJinja templates.

use std::collections::BTreeMap;
use std::fs;
use std::path::Path;

use anyhow::{Result, bail};
use minijinja::{Environment, context};

use crate::manifest::Manifest;
use serde_yaml;

/// Generate a new assets crate populated from the manifest.
pub(crate) fn run(path: &Path, manifest_path: &Path) -> Result<()> {
    if path.exists() {
        bail!("`{}` already exists", path.display());
    }

    let crate_name = path
        .file_name()
        .ok_or_else(|| anyhow::anyhow!("invalid crate path"))?
        .to_string_lossy()
        .into_owned();

    fs::create_dir_all(path.join("src"))?;

    let manifest: Manifest = if manifest_path.exists() {
        let contents = fs::read_to_string(manifest_path)?;
        serde_yaml::from_str(&contents)?
    } else {
        Manifest::default()
    };

    let mut groups: Vec<String> = manifest.groups.keys().map(|g| g.to_lowercase()).collect();
    groups.sort();

    // Map each asset path to the groups that reference it so constants can be feature gated.
    let mut per_asset_groups: BTreeMap<String, Vec<String>> = BTreeMap::new();
    for (group, info) in &manifest.groups {
        for asset in &info.assets {
            per_asset_groups
                .entry(asset.clone())
                .or_default()
                .push(group.to_lowercase());
        }
    }

    #[derive(serde::Serialize)]
    struct EmbedAsset {
        name: String,
        path: String,
        features: Vec<String>,
    }

    let mut embed_assets = Vec::new();
    for asset in &manifest.assets {
        embed_assets.push(EmbedAsset {
            name: asset.name.clone(),
            path: asset.path.clone(),
            features: per_asset_groups
                .get(&asset.path)
                .cloned()
                .unwrap_or_default(),
        });
    }

    let mut env = Environment::new();
    const CARGO_TOML: &str = r#"[package]
name = "{{ name }}"
version = "0.1.1"
edition = "2024"
description = "Assets crate generated by rlvgl-creator"

[features]
default = []
embed = []
vendor = []
{% for g in groups %}{{ g }} = []
{% endfor %}

[dependencies]
phf = { version = "0.11", features = ["macros"], default-features = false }
"#;
    const LIB_RS: &str = r#"#![no_std]
#![deny(missing_docs)]
//! Assets crate generated by rlvgl-creator.

#[cfg(feature = "vendor")]
extern crate std;

#[cfg(feature = "embed")]
/// Embedded asset bytes.
pub mod embed {
    use phf::{phf_map, Map};

    {% for a in assets %}
    {% if a.features | length > 0 %}
    #[cfg(any({% for f in a.features %}feature = "{{ f }}"{% if not loop.last %}, {% endif %}{% endfor %}))]
    {% endif %}
    pub const {{ a.name }}: &[u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../{{ a.path }}"));
    {% endfor %}

    static INDEX: Map<&'static str, &'static [u8]> = phf_map! {
    {% for a in assets %}
        "{{ a.path }}" => {{ a.name }},
    {% endfor %}
    };

    /// Fetch an embedded asset by its relative path.
    pub fn get(path: &str) -> Option<&'static [u8]> {
        INDEX.get(path).copied()
    }
}

#[cfg(feature = "vendor")]
/// Vendor build helpers.
pub mod vendor {
    use std::path::Path;

    /// Copy all assets into `out_dir`.
    pub fn copy_all(out_dir: &Path) -> std::io::Result<()> {
        vendor_api::copy_all(out_dir, ASSETS)
    }

    /// Generate an `rlvgl_assets.rs` module under `out_dir`.
    pub fn generate_rust_module(out_dir: &Path) -> std::io::Result<()> {
        vendor_api::generate_rust_module(out_dir, ASSETS)
    }

    const ASSETS: &[(&str, &str)] = &[
    {% for a in assets %}
        ("{{ a.name }}", "{{ a.path }}"),
    {% endfor %}
    ];

    mod vendor_api {
        use std::{fs, path::Path, string::String, format};

        pub fn copy_all(out_dir: &Path, assets: &[(&str, &str)]) -> std::io::Result<()> {
            let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../");
            for (_, path) in assets {
                let src = root.join(path);
                let dest = out_dir.join(path);
                if let Some(parent) = dest.parent() {
                    fs::create_dir_all(parent)?;
                }
                fs::copy(&src, &dest)?;
            }
            Ok(())
        }

        pub fn generate_rust_module(out_dir: &Path, assets: &[(&str, &str)]) -> std::io::Result<()> {
            let mut module = String::from("//! Auto-generated asset constants\n\n");
            for (name, path) in assets {
                module.push_str(&format!("pub const {}: &[u8] = include_bytes!(\"{}\");\n", name, path));
            }
            fs::write(out_dir.join("rlvgl_assets.rs"), module)?;
            Ok(())
        }
    }
}
"#;
    const README_MD: &str = r#"# {{ name }}

This crate was generated by `rlvgl-creator`.

## Embed

Enable the `embed` feature and reference asset constants directly:

```rust
use {{ name }}::embed::SOME_ASSET;
```

## Vendor

Enable the `vendor` feature to have the included `build.rs` copy asset files
and generate an `rlvgl_assets.rs` module at compile time. In your crate,
include the generated module:

```rust
include!(concat!(env!("OUT_DIR"), "/rlvgl_assets.rs"));
```

For custom pipelines, you may call the helpers from your own `build.rs`:

```rust
fn main() {
    let out = std::path::Path::new(&std::env::var("OUT_DIR").unwrap());
    {{ name }}::vendor::copy_all(out).unwrap();
    {{ name }}::vendor::generate_rust_module(out).unwrap();
}
```
"#;
    const BUILD_RS: &str = r#"// Build script for the assets crate.
//
// When built with the `vendor` feature this script copies asset files into the
// build output directory and generates an `rlvgl_assets.rs` module with
// `include_bytes!` declarations.

fn main() -> std::io::Result<()> {
    #[cfg(feature = "vendor")]
    {
        use std::path::Path;

        fn copy_all(out_dir: &Path, assets: &[(&str, &str)]) -> std::io::Result<()> {
            let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../");
            for (_, path) in assets {
                let src = root.join(path);
                let dest = out_dir.join(path);
                if let Some(parent) = dest.parent() {
                    std::fs::create_dir_all(parent)?;
                }
                std::fs::copy(&src, &dest)?;
                println!("cargo:rerun-if-changed={}", src.display());
            }
            Ok(())
        }

        fn generate_rust_module(out_dir: &Path, assets: &[(&str, &str)]) -> std::io::Result<()> {
            let mut module = String::from("//! Auto-generated asset constants\\n\\n");
            for (name, path) in assets {
                module.push_str(&format!("pub const {}: &[u8] = include_bytes!(\\\"{}\\\");\\n", name, path));
            }
            std::fs::write(out_dir.join("rlvgl_assets.rs"), module)?;
            Ok(())
        }

        const ASSETS: &[(&str, &str)] = &[
        {% for a in assets %}
            ("{{ a.name }}", "{{ a.path }}"),
        {% endfor %}
        ];

        let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
        copy_all(&out, ASSETS)?;
        generate_rust_module(&out, ASSETS)?;
    }
    Ok(())
}
"#;
    env.add_template("Cargo.toml", CARGO_TOML)?;
    env.add_template("lib.rs", LIB_RS)?;
    env.add_template("README.md", README_MD)?;
    env.add_template("build.rs", BUILD_RS)?;

    let ctx = context! { name => crate_name, groups => groups, assets => embed_assets };

    fs::write(
        path.join("Cargo.toml"),
        env.get_template("Cargo.toml")?.render(&ctx)?,
    )?;
    fs::write(
        path.join("src/lib.rs"),
        env.get_template("lib.rs")?.render(&ctx)?,
    )?;
    fs::write(
        path.join("README.md"),
        env.get_template("README.md")?.render(&ctx)?,
    )?;
    fs::write(
        path.join("build.rs"),
        env.get_template("build.rs")?.render(&ctx)?,
    )?;

    println!("Scaffolded crate `{}`", crate_name);
    Ok(())
}

#[cfg(all(test, feature = "regression"))]
mod tests {
    use super::*;
    use std::path::Path;
    use tempfile::tempdir;

    #[test]
    fn scaffold_generates_expected_files() {
        let dir = tempdir().unwrap();
        let crate_path = dir.path().join("test_assets");
        // Use a missing manifest path so defaults are applied.
        run(&crate_path, Path::new("missing.yml")).unwrap();

        let read = |p: &str| std::fs::read_to_string(crate_path.join(p)).unwrap();

        insta::assert_snapshot!("cargo_toml", read("Cargo.toml"));
        insta::assert_snapshot!("lib_rs", read("src/lib.rs"));
        insta::assert_snapshot!("readme_md", read("README.md"));
        insta::assert_snapshot!("build_rs", read("build.rs"));

        // Generate an empty `rlvgl_assets.rs` module and snapshot its contents.
        let out_dir = crate_path.join("out");
        std::fs::create_dir_all(&out_dir).unwrap();
        generate_rust_module(&out_dir, &[]).unwrap();
        let read_out = |p: &str| std::fs::read_to_string(out_dir.join(p)).unwrap();
        insta::assert_snapshot!("rlvgl_assets_rs", read_out("rlvgl_assets.rs"));
    }

    #[test]
    fn scaffold_cargo_publish_dry_run_succeeds() {
        let dir = tempdir().unwrap();
        let crate_path = dir.path().join("publishable_assets");
        run(&crate_path, Path::new("missing.yml")).unwrap();

        let status = std::process::Command::new("cargo")
            .arg("publish")
            .arg("--dry-run")
            .arg("--allow-dirty")
            .current_dir(&crate_path)
            .status()
            .expect("failed to run cargo publish");

        assert!(status.success());
    }

    #[test]
    fn scaffold_cargo_check_embed_and_vendor_succeed() {
        let dir = tempdir().unwrap();
        let crate_path = dir.path().join("buildable_assets");
        run(&crate_path, Path::new("missing.yml")).unwrap();

        let check = |features: &str| {
            let status = std::process::Command::new("cargo")
                .arg("check")
                .arg("--features")
                .arg(features)
                .current_dir(&crate_path)
                .status()
                .expect("failed to run cargo check");
            assert!(status.success());
        };

        check("embed");
        check("vendor");
    }

    #[test]
    fn embed_and_vendor_yield_identical_bytes() {
        use crate::{raw, scan, vendor};
        use image::{DynamicImage, Rgba, RgbaImage};
        use serde_yaml;

        let dir = tempdir().unwrap();
        let root = dir.path();

        // Create a tiny raw asset under the required root.
        let icons = root.join("icons");
        std::fs::create_dir_all(&icons).unwrap();
        let img = DynamicImage::ImageRgba8(RgbaImage::from_pixel(1, 1, Rgba([1, 2, 3, 4])));
        raw::encode_image(img, icons.join("test.raw")).unwrap();

        // Scan the asset and mark it with a license so vendoring succeeds.
        let manifest_path = root.join("manifest.yml");
        scan::run(root, &manifest_path).unwrap();
        let mut manifest: crate::manifest::Manifest =
            serde_yaml::from_str(&std::fs::read_to_string(&manifest_path).unwrap()).unwrap();
        manifest.assets[0].license = Some("MIT".to_string());
        std::fs::write(&manifest_path, serde_yaml::to_string(&manifest).unwrap()).unwrap();

        // Scaffold an assets crate and vendor the asset.
        let crate_path = root.join("test_assets");
        run(&crate_path, &manifest_path).unwrap();
        let out_dir = root.join("out");
        vendor::run(root, &manifest_path, &out_dir, &["MIT".into()], &[]).unwrap();

        // Embedded bytes should match vendored bytes for the same asset.
        let embed = std::fs::read(root.join("icons/test.raw")).unwrap();
        let vendored = std::fs::read(out_dir.join("icons/test.raw")).unwrap();
        assert_eq!(embed, vendored);
    }

    fn generate_rust_module(out_dir: &Path, assets: &[(&str, &str)]) -> std::io::Result<()> {
        let mut module = String::from("//! Auto-generated asset constants\n\n");
        for (name, path) in assets {
            module.push_str(&format!(
                "pub const {}: &[u8] = include_bytes!(\"{}\");\n",
                name, path
            ));
        }
        std::fs::write(out_dir.join("rlvgl_assets.rs"), module)
    }
}