use std::collections::{HashMap, HashSet}; use std::{ env, fs::{self, File}, io::{self, Write}, path::{Path, PathBuf}, }; pub fn build_static_assets(out_dir: &Path) { // Determine the application root directory using Cargo environment variables let Some(app_dir) = find_app_directory(out_dir) else { eprintln!("Error: Could not determine application directory"); return; }; let app_dir_str = app_dir.to_string_lossy().to_string(); println!("cargo:warning=Building with embedded_assets feature"); println!("cargo:warning=Application directory: {app_dir_str}"); println!("cargo:warning=Assets will only be loaded from the application directory"); println!("cargo:rerun-if-changed={app_dir_str}/assets/"); println!("cargo:rerun-if-changed={app_dir_str}/src/assets/"); // Also run build script again if the build files change println!("cargo:rerun-if-changed=build/embedded_assets.rs"); let generated_path = out_dir.join("generated_code"); // Create the directory if it doesn't exist if let Err(e) = fs::create_dir_all(&generated_path) { eprintln!("Warning: Could not create directory: {e}"); return; } // Only search in the application directory let app_root = app_dir; // Find all directories recursively, without filtering by name let all_dirs = discover_all_directories(&app_root.join("assets")); println!("cargo:warning=Discovered directories for assets:"); for dir in &all_dirs { println!("cargo:warning= - {}", dir.display()); } // Single collection for all files let mut all_files = HashMap::new(); // Store the assets directory reference to pass to collect_all_files let assets_dir = app_root.join("assets"); // Process all discovered directories for dir in &all_dirs { // Process all files in this directory collect_all_files(dir, &assets_dir, &mut all_files); } // Generate code for all assets if all_files.is_empty() { println!("cargo:warning=No asset files found"); // Generate empty asset files if no files found if let Err(e) = generate_empty_asset_files(&generated_path) { eprintln!("Warning: Failed to generate empty asset files: {e}"); } } else { println!("cargo:warning=Found {} asset files", all_files.len()); if let Err(e) = generate_asset_code(&all_files, &generated_path) { eprintln!("Warning: Failed to generate asset code: {e}"); } } } pub fn find_app_directory(out_dir: &Path) -> Option { // Find project root from OUT_DIR by going up to parent of "target" directory let mut path = out_dir.to_path_buf(); while path.pop() { if path.file_name().and_then(|n| n.to_str()) == Some("target") && path.pop() { return Some(path); } // Safety check if path.as_os_str().is_empty() { break; } } // Fallback to current directory env::current_dir().ok() } pub fn discover_all_directories(app_root: &Path) -> Vec { let mut directories = Vec::new(); let mut visited = HashSet::new(); // Only include the directory if it exists if app_root.exists() { // Add the root directory itself directories.push(app_root.to_path_buf()); // Start recursive discovery recursively_collect_directories(app_root, &mut directories, &mut visited); } // Sort directories by their string representation to ensure consistent ordering directories.sort_by(|a, b| { a.to_string_lossy() .to_string() .cmp(&b.to_string_lossy().to_string()) }); directories } pub fn recursively_collect_directories( dir: &Path, directories: &mut Vec, visited: &mut std::collections::HashSet, ) { // Check if we've already visited this directory if !visited.insert(dir.to_path_buf()) { return; } // Continue recursively discovering subdirectories if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { // Add this directory to our list directories.push(path.clone()); // Continue recursion recursively_collect_directories(&path, directories, visited); } } } } pub fn collect_all_files(dir: &Path, assets_dir: &Path, all_files: &mut HashMap) { if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_file() { // Skip if we can't determine the file path or extension let full_path = path.to_string_lossy().to_string(); // Create a relative path based on the assets directory let Ok(rel_path) = path.strip_prefix(assets_dir) else { println!( "cargo:warning=Failed to strip prefix for path: {}", path.display() ); continue; // Skip this file if we can't determine its relative path }; // Format the key as a path, using forward slashes let mut key = format!("/{}", rel_path.to_string_lossy().replace('\\', "/")); // Remove any double slashes key = key.replace("//", "/"); // Special handling for templates in views directory if key.starts_with("/views/") { // For templates, we want to: // 1. Strip "/views/" prefix for proper Tera template inheritance // 2. Keep the relative path structure for nested templates key = key.trim_start_matches("/views/").to_string(); } // Log what we found println!("cargo:warning=Found asset: {} -> {}", path.display(), key); // Store the file all_files.insert(full_path, key); } } } } #[allow(clippy::too_many_lines)] pub fn generate_asset_code( all_files: &HashMap, output_path: &Path, ) -> io::Result<()> { // Create vectors to track which files go where let mut static_assets = Vec::new(); let mut template_files = Vec::new(); // Simple categorization: if file ends with .html or .htm, it's a template, otherwise static asset for (path, key) in all_files { if std::path::Path::new(key) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("html")) || std::path::Path::new(key) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("htm")) { template_files.push((path.clone(), key.clone())); } else { static_assets.push((path.clone(), key.clone())); } } // Sort static assets by key for consistent output static_assets.sort_by(|a, b| a.1.cmp(&b.1)); // Build template dependency map and sort templates let mut template_deps: HashMap> = HashMap::new(); println!("cargo:warning=Analyzing template dependencies..."); // First pass: read all template contents and find their dependencies for (path, key) in &template_files { println!("cargo:warning=Reading template: {key}"); match fs::read_to_string(path) { Ok(content) => { // Look for {% extends "..." %} pattern if let Some(extends) = content .lines() .find(|line| line.trim().starts_with("{% extends")) { if let Some(parent) = extends .split('"') .nth(1) .or_else(|| extends.split('\'').nth(1)) { template_deps.insert(key.clone(), Some(parent.to_string())); println!("cargo:warning=Template {key} extends {parent}"); } } else { template_deps.insert(key.clone(), None); println!("cargo:warning=Template {key} has no parent"); } } Err(e) => { println!("cargo:warning=Failed to read template {path}: {e}"); } } } println!("cargo:warning=Template dependencies:"); for (template, parent) in &template_deps { if let Some(p) = parent { println!("cargo:warning= {template} -> {p}"); } else { println!("cargo:warning= {template} (no parent)"); } } // Sort templates so that parents come before children let mut sorted_templates = Vec::new(); let mut processed = HashSet::new(); // First add all base templates (those with no parents), sorted alphabetically let mut base_templates: Vec<_> = template_deps .iter() .filter(|(_, parent)| parent.is_none()) .map(|(key, _)| key.clone()) .collect(); base_templates.sort(); // Sort base templates alphabetically for key in base_templates { println!("cargo:warning=Adding base template: {key}"); processed.insert(key.clone()); sorted_templates.push(key); } // Then add all child templates, level by level let mut added_in_this_pass; while { added_in_this_pass = false; let mut level_templates = Vec::new(); // Collect all templates at this level for (key, parent) in &template_deps { if processed.contains(key) { continue; } if let Some(parent) = parent && processed.contains(parent) { level_templates.push(key.clone()); } } // Sort templates at this level alphabetically level_templates.sort(); // Add them to the final list for key in level_templates { if let Some(Some(parent)) = template_deps.get(&key) { println!("cargo:warning=Adding child template: {key} (extends {parent})"); } processed.insert(key.clone()); sorted_templates.push(key); added_in_this_pass = true; } added_in_this_pass } {} // Add any remaining templates that weren't processed, sorted alphabetically let mut remaining: Vec<_> = template_deps .keys() .filter(|key| !processed.contains(*key)) .cloned() .collect(); remaining.sort(); for key in remaining { println!("cargo:warning=Adding unprocessed template: {key}"); sorted_templates.push(key); } println!("cargo:warning=Final template order:"); for (idx, template) in sorted_templates.iter().enumerate() { println!("cargo:warning= {}. {}", idx + 1, template); } // Generate static assets file let static_file = output_path.join("static_assets.rs"); // Create the static assets content let mut static_lines = vec![ "#[must_use]\n".to_string(), "pub fn get_embedded_static_assets() -> std::collections::HashMap {\n".to_string(), " let mut assets = std::collections::HashMap::new();\n".to_string() ]; for (path, key) in &static_assets { let insert_line = format!( r#" assets.insert("{0}".to_string(), include_bytes!("{1}") as &[u8]);"#, key, path.replace('\\', "/") ); static_lines.push(format!("{insert_line}\n")); } static_lines.push(" assets\n".to_string()); static_lines.push("}\n".to_string()); // Write static assets content to file let mut static_file = File::create(static_file)?; for line in static_lines { static_file.write_all(line.as_bytes())?; } // Generate templates file let templates_file = output_path.join("view_templates.rs"); // Create the templates content with detailed comments let mut template_lines = vec![ "/// Returns a BTreeMap of templates in dependency order (parents before children)\n" .to_string(), "#[must_use]\n".to_string(), "pub fn get_embedded_templates() -> std::collections::BTreeMap {\n" .to_string(), " let mut templates = std::collections::BTreeMap::new();\n".to_string(), ]; // Add templates in dependency order with comments for template_key in &sorted_templates { if let Some((path, _)) = template_files.iter().find(|(_, k)| k == template_key) { // Add a comment showing the dependency if let Some(Some(parent)) = template_deps.get(template_key) { template_lines.push(format!(" // Template that extends {parent}\n")); } else { template_lines.push(" // Base template with no parent\n".to_string()); } let insert_line = format!( r#" templates.insert("{0}".to_string(), include_str!("{1}"));"#, template_key, path.replace('\\', "/") ); template_lines.push(format!("{insert_line}\n")); } } template_lines.push("\n templates\n".to_string()); template_lines.push("}\n".to_string()); // Write templates content to file let mut templates_file = File::create(templates_file)?; for line in template_lines { templates_file.write_all(line.as_bytes())?; } println!( "cargo:warning=Generated code for {} static assets and {} templates", static_assets.len(), sorted_templates.len() ); Ok(()) } pub fn generate_empty_asset_files(output_path: &Path) -> io::Result<()> { // Generate empty static assets file let static_file = output_path.join("static_assets.rs"); let static_code = r"#[must_use] pub fn get_embedded_static_assets() -> std::collections::HashMap { // No assets found std::collections::HashMap::new() } "; let mut file = File::create(static_file)?; file.write_all(static_code.as_bytes())?; // Generate empty templates file let templates_file = output_path.join("view_templates.rs"); let templates_code = r"#[must_use] pub fn get_embedded_templates() -> std::collections::HashMap { // No templates found std::collections::HashMap::new() } "; let mut file = File::create(templates_file)?; file.write_all(templates_code.as_bytes())?; Ok(()) }