223 lines
8.2 KiB
Rust
223 lines
8.2 KiB
Rust
|
|
//! 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<std::path::PathBuf, Box<dyn std::error::Error>> {
|
||
|
|
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<dyn std::error::Error>> {
|
||
|
|
// 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<dyn std::error::Error>> {
|
||
|
|
use std::collections::HashMap;
|
||
|
|
use std::path::Path;
|
||
|
|
use std::{env, fs};
|
||
|
|
|
||
|
|
let mut all_routes: HashMap<String, HashMap<String, String>> = 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<dyn std::error::Error>> {
|
||
|
|
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<String, HashSet<String>> = 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::<Vec<_>>()
|
||
|
|
.join("\n");
|
||
|
|
|
||
|
|
println!(
|
||
|
|
"cargo:warning=Menu validation errors in {}:\n{}",
|
||
|
|
path.display(),
|
||
|
|
error_messages
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|