pub mod path; use alloc::{ collections::BTreeMap, string::{String, ToString}, vec::Vec, }; use anyhow::{Context, Result, anyhow}; use globset::{Glob, GlobSet, GlobSetBuilder}; use path::unix_path_serde_option; use typed_path::Utf8UnixPathBuf; #[derive(Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))] pub struct ProjectConfig { #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub min_version: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub custom_make: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub custom_args: Option>, #[cfg_attr( feature = "serde", serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") )] pub target_dir: Option, #[cfg_attr( feature = "serde", serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") )] pub base_dir: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub build_base: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub build_target: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub watch_patterns: Option>, #[cfg_attr( feature = "serde", serde(alias = "objects", skip_serializing_if = "Option::is_none") )] pub units: Option>, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub progress_categories: Option>, } impl ProjectConfig { #[inline] pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() } #[inline] pub fn progress_categories(&self) -> &[ProjectProgressCategory] { self.progress_categories.as_deref().unwrap_or_default() } #[inline] pub fn progress_categories_mut(&mut self) -> &mut Vec { self.progress_categories.get_or_insert_with(Vec::new) } pub fn build_watch_patterns(&self) -> Result, globset::Error> { Ok(if let Some(watch_patterns) = &self.watch_patterns { watch_patterns .iter() .map(|s| Glob::new(s)) .collect::, globset::Error>>()? } else { DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() }) } } #[derive(Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))] pub struct ProjectObject { #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub name: Option, #[cfg_attr( feature = "serde", serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") )] pub path: Option, #[cfg_attr( feature = "serde", serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") )] pub target_path: Option, #[cfg_attr( feature = "serde", serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") )] pub base_path: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[deprecated(note = "Use metadata.reverse_fn_order")] pub reverse_fn_order: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[deprecated(note = "Use metadata.complete")] pub complete: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub scratch: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub metadata: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub symbol_mappings: Option>, } #[derive(Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))] pub struct ProjectObjectMetadata { #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub complete: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub reverse_fn_order: Option, #[cfg_attr( feature = "serde", serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") )] pub source_path: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub progress_categories: Option>, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub auto_generated: Option, } #[derive(Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))] pub struct ProjectProgressCategory { pub id: String, 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.as_str() } else { "[unknown]" } } pub fn complete(&self) -> Option { #[expect(deprecated)] self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) } pub fn reverse_fn_order(&self) -> Option { #[expect(deprecated)] self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order) } pub fn hidden(&self) -> bool { self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false) } pub fn source_path(&self) -> Option<&Utf8UnixPathBuf> { self.metadata.as_ref().and_then(|m| m.source_path.as_ref()) } pub fn progress_categories(&self) -> &[String] { self.metadata.as_ref().and_then(|m| m.progress_categories.as_deref()).unwrap_or_default() } pub fn auto_generated(&self) -> Option { self.metadata.as_ref().and_then(|m| m.auto_generated) } } #[derive(Default, Clone, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))] pub struct ScratchConfig { #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub platform: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub compiler: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub c_flags: Option, #[cfg_attr( feature = "serde", serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") )] pub ctx_path: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub build_ctx: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub preset_id: Option, } 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", ]; pub fn default_watch_patterns() -> Vec { DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() } #[cfg(feature = "std")] #[derive(Clone, Eq, PartialEq)] pub struct ProjectConfigInfo { pub path: std::path::PathBuf, pub timestamp: Option, } #[cfg(feature = "std")] pub fn try_project_config( dir: &std::path::Path, ) -> Option<(Result, ProjectConfigInfo)> { for filename in CONFIG_FILENAMES.iter() { let config_path = dir.join(filename); let Ok(file) = std::fs::File::open(&config_path) else { continue; }; let metadata = file.metadata(); if let Ok(metadata) = metadata { if !metadata.is_file() { continue; } let ts = filetime::FileTime::from_last_modification_time(&metadata); let mut reader = std::io::BufReader::new(file); let mut result = read_json_config(&mut reader); 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: Some(ts) })); } } None } #[cfg(feature = "std")] pub fn save_project_config( config: &ProjectConfig, info: &ProjectConfigInfo, ) -> Result { if let Some(last_ts) = info.timestamp { // Check if the file has changed since we last read it if let Ok(metadata) = std::fs::metadata(&info.path) { let ts = filetime::FileTime::from_last_modification_time(&metadata); if ts != last_ts { return Err(anyhow!("Config file has changed since last read")); } } } let mut writer = std::io::BufWriter::new( std::fs::File::create(&info.path).context("Failed to create config file")?, ); let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json"); match ext { "json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"), _ => Err(anyhow!("Unknown config file extension: {ext}")), }?; let file = writer.into_inner().context("Failed to flush file")?; let metadata = file.metadata().context("Failed to get file metadata")?; let ts = filetime::FileTime::from_last_modification_time(&metadata); Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) }) } 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")) .map_err(|e| anyhow::Error::msg(e.to_string())) .context("Failed to parse package version")?; let min_version = semver::Version::parse(min_version) .map_err(|e| anyhow::Error::msg(e.to_string())) .context("Failed to parse min_version")?; if version >= min_version { Ok(()) } else { Err(anyhow!("Project requires objdiff version {min_version} or higher")) } } #[cfg(feature = "std")] fn read_json_config(reader: &mut R) -> Result { Ok(serde_json::from_reader(reader)?) } pub fn build_globset(vec: &[Glob]) -> Result { let mut builder = GlobSetBuilder::new(); for glob in vec { builder.add(glob.clone()); } builder.build() }