use std::{ collections::BTreeMap, env, fs, io, path::{Path, PathBuf}, }; const EMPTY_DIRS_FILE: &str = ".empty-dirs"; fn main() { println!("cargo:rerun-if-changed=examples"); let examples_root = PathBuf::from("examples"); let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is not set")); let out_file = out_dir.join("embedded_examples.rs"); let mut dirs = Vec::new(); let mut files = Vec::new(); let mut empty_dirs_by_example = BTreeMap::new(); if examples_root.is_dir() { collect_examples( &examples_root, &examples_root, &mut dirs, &mut files, &mut empty_dirs_by_example, ) .expect("failed to collect example templates"); } dirs.sort(); dirs.dedup(); files.sort_by(|a, b| a.0.cmp(&b.0)); for empty_dirs in empty_dirs_by_example.values_mut() { empty_dirs.sort(); empty_dirs.dedup(); } let mut generated = String::new(); generated.push_str( "#[allow(dead_code)]\nstruct EmbeddedFile {\n path: &'static str,\n contents: &'static [u8],\n}\n\n", ); generated.push_str("#[allow(dead_code)]\nstatic EMBEDDED_EXAMPLE_DIRS: &[&str] = &[\n"); for dir in &dirs { generated.push_str(&format!(" {:?},\n", dir)); } generated.push_str("];\n\n"); generated.push_str("#[allow(dead_code)]\nstatic EMBEDDED_EXAMPLE_FILES: &[EmbeddedFile] = &[\n"); for (relative_path, absolute_path) in &files { generated.push_str(&format!( " EmbeddedFile {{ path: {:?}, contents: include_bytes!(r#\"{}\"#) }},\n", relative_path, absolute_path.display() )); } generated.push_str("];\n\n"); generated.push_str( "#[allow(dead_code)]\nstruct EmbeddedExampleEmptyDirs {\n example: &'static str,\n dirs: &'static [&'static str],\n}\n\n", ); generated.push_str( "#[allow(dead_code)]\nstatic EMBEDDED_EXAMPLE_EMPTY_DIRS: &[EmbeddedExampleEmptyDirs] = &[\n", ); for (example, empty_dirs) in &empty_dirs_by_example { generated.push_str(&format!( " EmbeddedExampleEmptyDirs {{ example: {:?}, dirs: &[\n", example )); for dir in empty_dirs { generated.push_str(&format!(" {:?},\n", dir)); } generated.push_str(" ] },\n"); } generated.push_str("];\n"); fs::write(out_file, generated).expect("failed to write embedded example list"); } fn collect_examples( root: &Path, dir: &Path, dirs: &mut Vec, files: &mut Vec<(String, PathBuf)>, empty_dirs_by_example: &mut BTreeMap>, ) -> io::Result<()> { for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); let relative_path = path .strip_prefix(root) .expect("example path is outside examples root") .to_string_lossy() .replace('\\', "/"); if path.is_dir() { dirs.push(relative_path); collect_examples(root, &path, dirs, files, empty_dirs_by_example)?; } else if path.is_file() { if path.file_name().and_then(|name| name.to_str()) == Some(EMPTY_DIRS_FILE) { collect_empty_dirs(&relative_path, &path, dirs, empty_dirs_by_example)?; } else { files.push((relative_path, path.canonicalize()?)); } } } Ok(()) } fn collect_empty_dirs( relative_path: &str, path: &Path, dirs: &mut Vec, empty_dirs_by_example: &mut BTreeMap>, ) -> io::Result<()> { let (example, file_name) = relative_path.split_once('/').ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, format!("{} must be inside an example directory", EMPTY_DIRS_FILE), ) })?; if file_name != EMPTY_DIRS_FILE { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("unexpected {} path: {}", EMPTY_DIRS_FILE, relative_path), )); } let contents = fs::read_to_string(path)?; let empty_dirs = empty_dirs_by_example .entry(example.to_string()) .or_insert_with(Vec::new); for line in contents.lines() { let dir = line.trim(); if dir.is_empty() || dir.starts_with('#') { continue; } dirs.push(format!("{}/{}", example, dir)); empty_dirs.push(dir.to_string()); } Ok(()) }