use anyhow::{Context, Result}; use cargo_toml::{Manifest, Value}; use lazy_static::lazy_static; use std::{ path::{Path, PathBuf}, str::FromStr, }; lazy_static! { static ref ENABLE_LOGGING: bool = { option_env!("LEPTONIC_BUILD_ENABLE_LOGGING") .and_then(|v| str::parse::(v).ok()) .unwrap_or(false) }; static ref MIN_LOG_LEVEL: Level = { option_env!("LEPTONIC_BUILD_MIN_LOG_LEVEL") .and_then(|v| str::parse::(v).ok()) .unwrap_or(Level::Debug) }; } #[derive(Debug)] struct LeptonicMetadata { relative_style_dir: String, relative_js_dir: String, } #[allow(clippy::unwrap_used)] pub fn main() -> Result<()> { let out_dir = get_out_dir().context("Could not find 'out_dir'.")?; let target_dir = get_cargo_target_dir(&out_dir).context("Could not find 'target_dir'.")?; let root_dir = target_dir .parent() .context("Expected 'target_dir' to have a parent.")? .to_owned(); log(Level::Debug, format!("root_dir is: {root_dir:?}")); let cargo_lock_path = root_dir.join("Cargo.lock"); let cargo_toml_path = root_dir.join("Cargo.toml"); assert!( cargo_toml_path.exists(), //.expect("Can't check existence of file Cargo.toml"), "Unable to find '{}'", cargo_toml_path.display() ); let metadata = match read_leptonic_metadata(&cargo_toml_path)? { Some(metadata) => metadata, None => return Ok(()), }; println!("cargo:rerun-if-changed={}", cargo_lock_path.display()); println!("cargo:rerun-if-changed={}", cargo_toml_path.display()); let style_dir = root_dir.join(&metadata.relative_style_dir); #[allow(unused_variables)] let js_dir = root_dir.join(&metadata.relative_js_dir); let theme_dir = style_dir.join("leptonic"); leptonic_theme::generate(&theme_dir).unwrap(); log( Level::Info, format!("theme written to {}", theme_dir.display()), ); #[cfg(feature = "tiptap")] copy_tiptap_files(&js_dir); Ok(()) } #[cfg(feature = "tiptap")] fn copy_tiptap_files(js_dir: &PathBuf) { use std::io::Write; std::fs::create_dir_all(js_dir).unwrap(); std::fs::File::create(js_dir.join("tiptap-bundle.min.js")) .unwrap() .write_all(leptos_tiptap_build::TIPTAP_BUNDLE_MIN_JS.as_bytes()) .unwrap(); log( Level::Info, format!("tiptap-bundle.min.js written to {}", js_dir.display()), ); std::fs::File::create(js_dir.join("tiptap.js")) .unwrap() .write_all(leptos_tiptap_build::TIPTAP_JS.as_bytes()) .unwrap(); log( Level::Info, format!("tiptap.js written to {}", js_dir.display()), ); } /// Parse the Cargo.toml file! Abort if the Cargo.toml has no config. fn read_leptonic_metadata(cargo_toml_path: &PathBuf) -> Result> { let cargo_toml: Manifest = cargo_toml::Manifest::from_path_with_metadata(&cargo_toml_path).with_context(|| { format!( "Could not parse Cargo.toml at '{}'", cargo_toml_path.display() ) })?; if cargo_toml.package.is_none() { log( Level::Debug, "Aborting. Root dir does not contain a package.", ); return Ok(None); } let meta = cargo_toml .package() .metadata .as_ref() .and_then(|m| m.get("leptonic")); if meta.is_none() { log( Level::Debug, "Aborting. Root dir is a package without specifying leptonic metadata.", ); return Ok(None); } let meta = meta.expect("present"); let table = meta .as_table() .context("Leptonic metadata was not of type 'table'.")?; let relative_style_dir = table .get("style-dir") .context("Leptonic's 'style-dir' metadata was not declared.")? .as_str() .context("Leptonic's 'style-dir' metadata was not of type 'string'.")? .to_owned(); let relative_js_dir = table .get("js-dir") .context("Leptonic's 'js-dir' metadata was not declared.")? .as_str() .context("Leptonic's 'js-dir' metadata was not of type 'string'.")? .to_owned(); log( Level::Debug, format!("relative_style_dir is: {relative_style_dir:?}"), ); log( Level::Debug, format!("relative_js_dir is: {relative_js_dir:?}"), ); Ok(Some(LeptonicMetadata { relative_style_dir, relative_js_dir, })) } fn get_out_dir() -> Result { let out_dir = PathBuf::from(std::env::var("OUT_DIR")?); log(Level::Debug, format!("out_dir is: {out_dir:?}")); Ok(out_dir) } // Credits @ssrlive (source: https://github.com/rust-lang/cargo/issues/9661) fn get_cargo_target_dir(out_dir: impl AsRef) -> Result { let mut target_dir = None; let mut sub_path = out_dir.as_ref(); while let Some(parent) = sub_path.parent() { if parent.ends_with("target") { target_dir = Some(parent); break; } sub_path = parent; } let target_dir = target_dir.with_context(|| { format!( "Could not find `target` dir in parents of {}", out_dir.as_ref().display() ) })?; Ok(target_dir.to_path_buf()) } fn log(level: Level, msg: impl AsRef) { let msg = msg.as_ref(); if *ENABLE_LOGGING && level >= *MIN_LOG_LEVEL { println!("cargo:warning=[{level}] {msg}"); } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[allow(dead_code)] enum Level { Debug = 0, Info = 1, Warn = 2, Error = 3, } impl FromStr for Level { type Err = String; fn from_str(s: &str) -> Result { match s { "debug" | "Debug" | "DEBUG" => Ok(Self::Debug), "info" | "Info" | "INFO" => Ok(Self::Info), "warn" | "Warn" | "WARN" => Ok(Self::Warn), "error" | "Error" | "ERROR" => Ok(Self::Error), _ => Err(format!("'{s}' is not a valid LogLevel.")), } } } impl std::fmt::Display for Level { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Debug => "DEBUG", Self::Info => "INFO", Self::Warn => "WARN", Self::Error => "ERROR", }) } }