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;
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();
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");
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"));
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();
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();
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();
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();
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)
}
}