mirror of
https://github.com/encounter/objdiff.git
synced 2025-06-08 07:33:39 +00:00
293 lines
11 KiB
Rust
293 lines
11 KiB
Rust
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<String>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub custom_make: Option<String>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub custom_args: Option<Vec<String>>,
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
|
)]
|
|
pub target_dir: Option<Utf8UnixPathBuf>,
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
|
)]
|
|
pub base_dir: Option<Utf8UnixPathBuf>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub build_base: Option<bool>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub build_target: Option<bool>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub watch_patterns: Option<Vec<String>>,
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
serde(alias = "objects", skip_serializing_if = "Option::is_none")
|
|
)]
|
|
pub units: Option<Vec<ProjectObject>>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
|
|
}
|
|
|
|
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<ProjectProgressCategory> {
|
|
self.progress_categories.get_or_insert_with(Vec::new)
|
|
}
|
|
|
|
pub fn build_watch_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
|
|
Ok(if let Some(watch_patterns) = &self.watch_patterns {
|
|
watch_patterns
|
|
.iter()
|
|
.map(|s| Glob::new(s))
|
|
.collect::<Result<Vec<Glob>, 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<String>,
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
|
)]
|
|
pub path: Option<Utf8UnixPathBuf>,
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
|
)]
|
|
pub target_path: Option<Utf8UnixPathBuf>,
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
|
)]
|
|
pub base_path: Option<Utf8UnixPathBuf>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
|
pub reverse_fn_order: Option<bool>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
#[deprecated(note = "Use metadata.complete")]
|
|
pub complete: Option<bool>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub scratch: Option<ScratchConfig>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub metadata: Option<ProjectObjectMetadata>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub symbol_mappings: Option<BTreeMap<String, String>>,
|
|
}
|
|
|
|
#[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<bool>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub reverse_fn_order: Option<bool>,
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
|
)]
|
|
pub source_path: Option<Utf8UnixPathBuf>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub progress_categories: Option<Vec<String>>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub auto_generated: Option<bool>,
|
|
}
|
|
|
|
#[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<bool> {
|
|
#[expect(deprecated)]
|
|
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
|
}
|
|
|
|
pub fn reverse_fn_order(&self) -> Option<bool> {
|
|
#[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<bool> {
|
|
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<String>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub compiler: Option<String>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub c_flags: Option<String>,
|
|
#[cfg_attr(
|
|
feature = "serde",
|
|
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
|
|
)]
|
|
pub ctx_path: Option<Utf8UnixPathBuf>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub build_ctx: Option<bool>,
|
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub preset_id: Option<u32>,
|
|
}
|
|
|
|
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<Glob> {
|
|
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<filetime::FileTime>,
|
|
}
|
|
|
|
#[cfg(feature = "std")]
|
|
pub fn try_project_config(
|
|
dir: &std::path::Path,
|
|
) -> Option<(Result<ProjectConfig>, 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<ProjectConfigInfo> {
|
|
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<R: std::io::Read>(reader: &mut R) -> Result<ProjectConfig> {
|
|
Ok(serde_json::from_reader(reader)?)
|
|
}
|
|
|
|
pub fn build_globset(vec: &[Glob]) -> Result<GlobSet, globset::Error> {
|
|
let mut builder = GlobSetBuilder::new();
|
|
for glob in vec {
|
|
builder.add(glob.clone());
|
|
}
|
|
builder.build()
|
|
}
|