2024-02-27 18:47:51 -07:00
|
|
|
use std::{
|
2024-10-09 21:44:18 -06:00
|
|
|
fs,
|
2024-02-27 18:47:51 -07:00
|
|
|
fs::File,
|
2024-10-09 21:44:18 -06:00
|
|
|
io::{BufReader, BufWriter, Read},
|
2024-02-27 18:47:51 -07:00
|
|
|
path::{Path, PathBuf},
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-21 21:36:23 -06:00
|
|
|
use anyhow::{anyhow, Context, Result};
|
2024-10-09 21:44:18 -06:00
|
|
|
use bimap::BiBTreeMap;
|
2024-02-27 18:47:51 -07:00
|
|
|
use filetime::FileTime;
|
|
|
|
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
|
|
|
|
|
2024-10-09 21:44:18 -06:00
|
|
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub struct ProjectConfig {
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub min_version: Option<String>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub custom_make: Option<String>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-05-16 02:53:14 +02:00
|
|
|
pub custom_args: Option<Vec<String>>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub target_dir: Option<PathBuf>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub base_dir: Option<PathBuf>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub build_base: Option<bool>,
|
|
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub build_target: Option<bool>,
|
|
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub watch_patterns: Option<Vec<Glob>>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub units: Option<Vec<ProjectObject>>,
|
|
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
|
2024-02-27 18:47:51 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-09 21:44:18 -06:00
|
|
|
impl ProjectConfig {
|
|
|
|
|
#[inline]
|
|
|
|
|
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
|
|
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
|
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
|
|
|
|
|
self.units.get_or_insert_with(Vec::new)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub struct ProjectObject {
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub name: Option<String>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub path: Option<PathBuf>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub target_path: Option<PathBuf>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub base_path: Option<PathBuf>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-09-03 00:59:06 -06:00
|
|
|
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub reverse_fn_order: Option<bool>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-09-03 00:59:06 -06:00
|
|
|
#[deprecated(note = "Use metadata.complete")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub complete: Option<bool>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub scratch: Option<ScratchConfig>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-09-03 00:59:06 -06:00
|
|
|
pub metadata: Option<ProjectObjectMetadata>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub symbol_mappings: Option<SymbolMappings>,
|
2024-09-03 00:59:06 -06:00
|
|
|
}
|
|
|
|
|
|
2024-10-09 21:44:18 -06:00
|
|
|
pub type SymbolMappings = BiBTreeMap<String, String>;
|
|
|
|
|
|
|
|
|
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
2024-09-03 00:59:06 -06:00
|
|
|
pub struct ProjectObjectMetadata {
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-09-03 00:59:06 -06:00
|
|
|
pub complete: Option<bool>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-09-03 00:59:06 -06:00
|
|
|
pub reverse_fn_order: Option<bool>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-09-03 00:59:06 -06:00
|
|
|
pub source_path: Option<String>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-09-03 00:59:06 -06:00
|
|
|
pub progress_categories: Option<Vec<String>>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-09-03 00:59:06 -06:00
|
|
|
pub auto_generated: Option<bool>,
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 21:44:18 -06:00
|
|
|
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
2024-09-03 00:59:06 -06:00
|
|
|
pub struct ProjectProgressCategory {
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub id: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub name: String,
|
2024-02-27 18:47:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-03 00:59:06 -06:00
|
|
|
|
|
|
|
|
pub fn complete(&self) -> Option<bool> {
|
2024-10-09 21:44:18 -06:00
|
|
|
#[expect(deprecated)]
|
2024-09-03 00:59:06 -06:00
|
|
|
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn reverse_fn_order(&self) -> Option<bool> {
|
2024-10-09 21:44:18 -06:00
|
|
|
#[expect(deprecated)]
|
2024-09-03 00:59:06 -06:00
|
|
|
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
|
|
|
|
}
|
2024-09-03 18:59:07 -06:00
|
|
|
|
|
|
|
|
pub fn hidden(&self) -> bool {
|
|
|
|
|
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
|
|
|
|
|
}
|
2024-09-28 10:55:25 -06:00
|
|
|
|
|
|
|
|
pub fn source_path(&self) -> Option<&String> {
|
|
|
|
|
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
|
|
|
|
|
}
|
2024-02-27 18:47:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
|
|
|
pub struct ScratchConfig {
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub platform: Option<String>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub compiler: Option<String>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub c_flags: Option<String>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
2024-02-27 18:47:51 -07:00
|
|
|
pub ctx_path: Option<PathBuf>,
|
2024-10-09 21:44:18 -06:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub build_ctx: Option<bool>,
|
2024-02-27 18:47:51 -07:00
|
|
|
}
|
|
|
|
|
|
2024-03-21 21:36:23 -06:00
|
|
|
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
|
2024-02-27 18:47:51 -07:00
|
|
|
|
|
|
|
|
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,
|
2024-10-09 21:44:18 -06:00
|
|
|
pub timestamp: Option<FileTime>,
|
2024-02-27 18:47:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
|
|
|
|
|
for filename in CONFIG_FILENAMES.iter() {
|
|
|
|
|
let config_path = dir.join(filename);
|
2024-09-28 10:54:54 -06:00
|
|
|
let Ok(file) = File::open(&config_path) else {
|
2024-02-27 18:47:51 -07:00
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let metadata = file.metadata();
|
|
|
|
|
if let Ok(metadata) = metadata {
|
|
|
|
|
if !metadata.is_file() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let ts = FileTime::from_last_modification_time(&metadata);
|
2024-09-28 10:54:54 -06:00
|
|
|
let mut reader = BufReader::new(file);
|
2024-03-21 21:36:23 -06:00
|
|
|
let mut result = match filename.contains("json") {
|
2024-09-28 10:54:54 -06:00
|
|
|
true => read_json_config(&mut reader),
|
|
|
|
|
false => read_yml_config(&mut reader),
|
2024-02-27 18:47:51 -07:00
|
|
|
};
|
2024-03-21 21:36:23 -06:00
|
|
|
if let Ok(config) = &result {
|
|
|
|
|
// Validate min_version if present
|
|
|
|
|
if let Err(e) = validate_min_version(config) {
|
|
|
|
|
result = Err(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-09 21:44:18 -06:00
|
|
|
return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
|
2024-02-27 18:47:51 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 21:44:18 -06:00
|
|
|
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) = fs::metadata(&info.path) {
|
|
|
|
|
let ts = FileTime::from_last_modification_time(&metadata);
|
|
|
|
|
if ts != last_ts {
|
|
|
|
|
return Err(anyhow!("Config file has changed since last read"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let mut writer =
|
|
|
|
|
BufWriter::new(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"),
|
|
|
|
|
"yml" | "yaml" => {
|
|
|
|
|
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
|
|
|
|
|
}
|
|
|
|
|
_ => 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::from_last_modification_time(&metadata);
|
|
|
|
|
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-21 21:36:23 -06:00
|
|
|
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")?;
|
2024-05-21 09:50:27 -06:00
|
|
|
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"))
|
2024-03-21 21:36:23 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-27 18:47:51 -07:00
|
|
|
fn read_yml_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
|
|
|
|
|
Ok(serde_yaml::from_reader(reader)?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_json_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
|
|
|
|
|
Ok(serde_json::from_reader(reader)?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {
|
|
|
|
|
let mut builder = GlobSetBuilder::new();
|
|
|
|
|
for glob in vec {
|
|
|
|
|
builder.add(glob.clone());
|
|
|
|
|
}
|
|
|
|
|
builder.build()
|
|
|
|
|
}
|