use std::{ fs::File, io::Read, path::{Path, PathBuf}, }; use anyhow::{anyhow, Context, Result}; use filetime::FileTime; use globset::{Glob, GlobSet, GlobSetBuilder}; #[inline] fn bool_true() -> bool { true } #[derive(Default, Clone, serde::Deserialize)] pub struct ProjectConfig { #[serde(default)] pub min_version: Option, #[serde(default)] pub custom_make: Option, #[serde(default)] pub custom_args: Option>, #[serde(default)] pub target_dir: Option, #[serde(default)] pub base_dir: Option, #[serde(default = "bool_true")] pub build_base: bool, #[serde(default)] pub build_target: bool, #[serde(default)] pub watch_patterns: Option>, #[serde(default, alias = "units")] pub objects: Vec, #[serde(default)] pub progress_categories: Vec, } #[derive(Default, Clone, serde::Deserialize)] pub struct ProjectObject { #[serde(default)] pub name: Option, #[serde(default)] pub path: Option, #[serde(default)] pub target_path: Option, #[serde(default)] pub base_path: Option, #[serde(default)] #[deprecated(note = "Use metadata.reverse_fn_order")] pub reverse_fn_order: Option, #[serde(default)] #[deprecated(note = "Use metadata.complete")] pub complete: Option, #[serde(default)] pub scratch: Option, #[serde(default)] pub metadata: Option, } #[derive(Default, Clone, serde::Deserialize)] pub struct ProjectObjectMetadata { #[serde(default)] pub complete: Option, #[serde(default)] pub reverse_fn_order: Option, #[serde(default)] pub source_path: Option, #[serde(default)] pub progress_categories: Option>, #[serde(default)] pub auto_generated: Option, } #[derive(Default, Clone, serde::Deserialize)] pub struct ProjectProgressCategory { #[serde(default)] pub id: String, #[serde(default)] pub name: String, } impl ProjectObject { pub fn name(&self) -> &str { if let Some(name) = &self.name { name } else if let Some(path) = &self.path { path.to_str().unwrap_or("[invalid path]") } else { "[unknown]" } } pub fn resolve_paths( &mut self, project_dir: &Path, target_obj_dir: Option<&Path>, base_obj_dir: Option<&Path>, ) { if let (Some(target_obj_dir), Some(path), None) = (target_obj_dir, &self.path, &self.target_path) { self.target_path = Some(target_obj_dir.join(path)); } else if let Some(path) = &self.target_path { self.target_path = Some(project_dir.join(path)); } if let (Some(base_obj_dir), Some(path), None) = (base_obj_dir, &self.path, &self.base_path) { self.base_path = Some(base_obj_dir.join(path)); } else if let Some(path) = &self.base_path { self.base_path = Some(project_dir.join(path)); } } pub fn complete(&self) -> Option { #[allow(deprecated)] self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) } pub fn reverse_fn_order(&self) -> Option { #[allow(deprecated)] self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order) } } #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct ScratchConfig { #[serde(default)] pub platform: Option, #[serde(default)] pub compiler: Option, #[serde(default)] pub c_flags: Option, #[serde(default)] pub ctx_path: Option, #[serde(default)] pub build_ctx: bool, } pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"]; pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[ "*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm", "*.inc", "*.py", "*.yml", "*.txt", "*.json", ]; #[derive(Clone, Eq, PartialEq)] pub struct ProjectConfigInfo { pub path: PathBuf, pub timestamp: FileTime, } pub fn try_project_config(dir: &Path) -> Option<(Result, ProjectConfigInfo)> { for filename in CONFIG_FILENAMES.iter() { let config_path = dir.join(filename); let Ok(mut file) = File::open(&config_path) else { continue; }; let metadata = file.metadata(); if let Ok(metadata) = metadata { if !metadata.is_file() { continue; } let ts = FileTime::from_last_modification_time(&metadata); let mut result = match filename.contains("json") { true => read_json_config(&mut file), false => read_yml_config(&mut file), }; if let Ok(config) = &result { // Validate min_version if present if let Err(e) = validate_min_version(config) { result = Err(e); } } return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts })); } } None } fn validate_min_version(config: &ProjectConfig) -> Result<()> { let Some(min_version) = &config.min_version else { return Ok(()) }; let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) .context("Failed to parse package version")?; let min_version = semver::Version::parse(min_version).context("Failed to parse min_version")?; if version >= min_version { Ok(()) } else { Err(anyhow!("Project requires objdiff version {min_version} or higher")) } } fn read_yml_config(reader: &mut R) -> Result { Ok(serde_yaml::from_reader(reader)?) } fn read_json_config(reader: &mut R) -> Result { Ok(serde_json::from_reader(reader)?) } pub fn build_globset(vec: &[Glob]) -> std::result::Result { let mut builder = GlobSetBuilder::new(); for glob in vec { builder.add(glob.clone()); } builder.build() }