//! Shared build script - Using Provider pattern with Rustelo foundation traits //! //! This build script uses the Provider pattern with dependency injection //! instead of direct function calls. Follows PAP principles completely. //! //! Includes build-time route validation to ensure menu routes match configured pages. /// Walk up from `CARGO_MANIFEST_DIR` to find the Cargo workspace root. /// /// Falls back to `CARGO_MANIFEST_DIR` itself when no ancestor contains `[workspace]`, /// which is correct for single-crate projects. fn find_workspace_root() -> Result> { let pkg_dir = std::env::var("CARGO_MANIFEST_DIR") .map_err(|_| "CARGO_MANIFEST_DIR not set — this function is only valid in build scripts")?; let mut current = std::path::PathBuf::from(pkg_dir); for _ in 0..15 { let cargo_toml = current.join("Cargo.toml"); if cargo_toml.exists() { if let Ok(content) = std::fs::read_to_string(&cargo_toml) { if content.contains("[workspace]") { return Ok(current); } } } current = current .parent() .ok_or("Reached filesystem root without finding a [workspace] Cargo.toml")? .to_path_buf(); } Err("Workspace root not found — ensure a Cargo.toml with [workspace] exists in the project hierarchy".into()) } fn main() -> Result<(), Box> { // Use new PAP-compliant manifest system // let _manifest = rustelo_utils::get_manifest(); let content_path = rustelo_utils::manifest_content_path(); let config_path = rustelo_utils::manifest_config_path(); let routes_path = rustelo_utils::routes_path(); let ui_path = rustelo_utils::ui_path(); println!("cargo:rerun-if-changed={}", routes_path.display()); println!("cargo:rerun-if-changed={}", config_path.display()); println!("cargo:rerun-if-changed={}", content_path.display()); println!("cargo:rerun-if-changed={}", ui_path.display()); // Trigger rebuild when unified site config changes. // ManifestResolver is preferred; if it fails (no rustelo.manifest.toml), derive from CARGO_MANIFEST_DIR. let manifest_dir = rustelo_utils::ManifestResolver::load() .map(|m| m.root().to_path_buf()) .or_else(|_| find_workspace_root())?; let site_ncl = manifest_dir.join("site").join("config").join("index.ncl"); if site_ncl.exists() { println!("cargo:rerun-if-changed={}", site_ncl.display()); } // Generate shared route constants generate_shared_routes()?; // Validate menu routes match configured routes validate_menu_routes(&routes_path, &ui_path)?; println!("cargo:warning=Shared build completed"); Ok(()) } fn generate_shared_routes() -> Result<(), Box> { use std::collections::HashMap; use std::path::Path; use std::{env, fs}; let mut all_routes: HashMap> = HashMap::new(); let manifest_dir = rustelo_utils::ManifestResolver::load() .map(|m| m.root().to_path_buf()) .or_else(|_| find_workspace_root()) .expect("Cannot determine workspace root — check CARGO_MANIFEST_DIR or add rustelo.manifest.toml"); let site_cfg = build_config::site_config::load_unified_site_config(&manifest_dir) .unwrap_or_else(|e| { panic!( "Cannot load site configuration: {e}\n\ Install Nickel: cargo install nickel-lang-cli" ) }); println!( "cargo:warning=Shared: Loaded site configuration ({} languages)", site_cfg.languages().len() ); for (lang, routes) in site_cfg.routes_expanded() { let mut lang_routes = HashMap::new(); for route in routes { lang_routes.insert(route.component.to_lowercase(), route.path.clone()); } all_routes.insert(lang, lang_routes); } // Generate Rust code for route constants let mut generated_code = String::new(); generated_code.push_str("// Auto-generated route constants\n"); generated_code.push_str("// Generated from manifest routes\n\n"); generated_code.push_str("pub mod routes {\n"); for (lang, routes) in &all_routes { generated_code.push_str(&format!(" pub mod {} {{\n", lang)); for (route_name, path) in routes { let const_name = route_name.to_uppercase(); generated_code.push_str(&format!( " pub const {}: &str = \"{}\";\n", const_name, path )); } generated_code.push_str(" }\n\n"); } generated_code.push_str("}\n"); // Write the generated routes let out_dir = env::var("OUT_DIR")?; let routes_file = Path::new(&out_dir).join("routes_generated.rs"); fs::write(routes_file, generated_code)?; Ok(()) } /// Validate that all menu routes point to configured pages fn validate_menu_routes( routes_path: &std::path::Path, ui_path: &std::path::Path, ) -> Result<(), Box> { use std::collections::{HashMap, HashSet}; use std::fs; let menus_path = ui_path.join("menus"); // If menus directory doesn't exist, skip validation if !menus_path.exists() { return Ok(()); } // Load all configured routes by language using typed loading let mut configured_routes: HashMap> = HashMap::new(); if routes_path.exists() { // Use typed route loading (NCL-first, TOML-fallback) match build_config::route_config::load_all_routes(routes_path) { Ok(routes_by_lang) => { for (lang, routes) in routes_by_lang { let mut lang_routes = HashSet::new(); for route in routes { // Collect all route paths for validation lang_routes.insert(route.path.clone()); } configured_routes.insert(lang, lang_routes); } } Err(e) => { println!( "cargo:warning=Failed to load routes for menu validation: {}", e ); // Continue without validation if routes can't be loaded return Ok(()); } } } // Create route validator let validator = rustelo_utils::RouteValidator::new(configured_routes); // Validate all menu files for entry in fs::read_dir(&menus_path)? { let entry = entry?; if entry.path().extension().is_some_and(|ext| ext == "toml") { let path = entry.path(); if let Some(language) = path.file_stem().and_then(|s| s.to_str()) { let menu_content = fs::read_to_string(&path)?; let menu: toml::Value = toml::from_str(&menu_content)?; if let Some(menu_items) = menu.get("menu").and_then(|v| v.as_array()) { let mut route_checks = Vec::new(); for menu_item in menu_items { if let Some(route) = menu_item.get("route").and_then(|v| v.as_str()) { let is_external = menu_item .get("is_external") .and_then(|v| v.as_bool()) .unwrap_or(false); route_checks.push(( route.to_string(), language.to_string(), is_external, )); } } // Validate batch if let Err(errors) = validator.validate_batch(&route_checks) { let error_messages = errors .iter() .map(|e| format!(" - {}", e)) .collect::>() .join("\n"); println!( "cargo:warning=Menu validation errors in {}:\n{}", path.display(), error_messages ); } } } } } Ok(()) }