Files
objdiff/objdiff-cli/src/cmd/diff.rs
T

452 lines
16 KiB
Rust
Raw Normal View History

2024-08-20 21:40:32 -06:00
use std::{
fs,
io::stdout,
2024-10-11 18:37:14 -06:00
mem,
2024-08-20 21:40:32 -06:00
path::{Path, PathBuf},
str::FromStr,
2024-10-11 18:37:14 -06:00
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
task::{Wake, Waker},
time::Duration,
2024-08-20 21:40:32 -06:00
};
2024-02-27 18:47:51 -07:00
2024-12-29 16:57:18 -07:00
use anyhow::{anyhow, bail, Context, Result};
2024-02-27 18:47:51 -07:00
use argp::FromArgs;
use crossterm::{
event,
2024-10-11 18:37:14 -06:00
event::{DisableMouseCapture, EnableMouseCapture},
2024-02-27 18:47:51 -07:00
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle,
2024-02-27 18:47:51 -07:00
},
};
use objdiff_core::{
2024-08-20 21:40:32 -06:00
bindings::diff::DiffResult,
2024-10-11 18:37:14 -06:00
build::{
watcher::{create_watcher, Watcher},
BuildConfig,
},
2024-12-29 16:57:18 -07:00
config::{build_globset, ProjectConfig, ProjectObject},
2024-02-27 18:47:51 -07:00
diff,
2024-12-29 16:57:18 -07:00
diff::{
ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig, ObjDiff,
},
2024-10-11 18:37:14 -06:00
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult,
},
2024-02-27 18:47:51 -07:00
obj,
2024-10-11 18:37:14 -06:00
obj::ObjInfo,
};
2024-10-11 18:37:14 -06:00
use ratatui::prelude::*;
2024-02-27 18:47:51 -07:00
2024-10-11 18:37:14 -06:00
use crate::{
util::{
output::{write_output, OutputFormat},
term::crossterm_panic_handler,
},
views::{function_diff::FunctionDiffUi, EventControlFlow, EventResult, UiView},
2024-08-20 21:40:32 -06:00
};
2024-02-27 18:47:51 -07:00
#[derive(FromArgs, PartialEq, Debug)]
2024-08-20 21:40:32 -06:00
/// Diff two object files. (Interactive or one-shot mode)
2024-02-27 18:47:51 -07:00
#[argp(subcommand, name = "diff")]
pub struct Args {
#[argp(option, short = '1')]
2024-02-27 18:47:51 -07:00
/// Target object file
target: Option<PathBuf>,
#[argp(option, short = '2')]
2024-02-27 18:47:51 -07:00
/// Base object file
base: Option<PathBuf>,
#[argp(option, short = 'p')]
/// Project directory
project: Option<PathBuf>,
#[argp(option, short = 'u')]
/// Unit name within project
unit: Option<String>,
2024-08-20 21:40:32 -06:00
#[argp(option, short = 'o')]
/// Output file (one-shot mode) ("-" for stdout)
output: Option<PathBuf>,
#[argp(option)]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
#[argp(positional)]
2024-02-27 18:47:51 -07:00
/// Function symbol to diff
2024-08-20 21:40:32 -06:00
symbol: Option<String>,
2024-12-29 16:57:18 -07:00
#[argp(option, short = 'c')]
/// Configuration property (key=value)
config: Vec<String>,
#[argp(option, short = 'm')]
/// Symbol mapping (target=base)
mapping: Vec<String>,
#[argp(option)]
/// Left symbol name for selection
selecting_left: Option<String>,
#[argp(option)]
/// Right symbol name for selection
selecting_right: Option<String>,
2024-02-27 18:47:51 -07:00
}
pub fn run(args: Args) -> Result<()> {
2024-08-20 21:40:32 -06:00
let (target_path, base_path, project_config) = match (
&args.target,
&args.base,
&args.project,
&args.unit,
) {
2024-12-29 16:57:18 -07:00
(Some(_), Some(_), None, None)
| (Some(_), None, None, None)
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
2024-08-20 21:40:32 -06:00
(None, None, p, u) => {
let project = match p {
Some(project) => project.clone(),
_ => std::env::current_dir().context("Failed to get the current directory")?,
};
let Some((project_config, project_config_info)) =
objdiff_core::config::try_project_config(&project)
else {
bail!("Project config not found in {}", &project.display())
};
let mut project_config = project_config.with_context(|| {
format!("Reading project config {}", project_config_info.path.display())
})?;
let object = {
let resolve_paths = |o: &mut ProjectObject| {
o.resolve_paths(
&project,
project_config.target_dir.as_deref(),
project_config.base_dir.as_deref(),
)
};
2024-08-20 21:40:32 -06:00
if let Some(u) = u {
let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
2024-10-09 21:44:18 -06:00
let Some(object) = project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.find_map(|obj| {
if obj.name.as_deref() == Some(u) {
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
resolve_paths(obj);
2024-08-20 21:40:32 -06:00
2024-10-09 21:44:18 -06:00
if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
2024-08-20 21:40:32 -06:00
2024-10-09 21:44:18 -06:00
None
})
else {
2024-08-20 21:40:32 -06:00
bail!("Unit not found: {}", u)
};
object
} else if let Some(symbol_name) = &args.symbol {
let mut idx = None;
let mut count = 0usize;
2024-10-09 21:44:18 -06:00
for (i, obj) in project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.enumerate()
{
2024-08-20 21:40:32 -06:00
resolve_paths(obj);
if obj
.target_path
.as_deref()
.map(|o| obj::read::has_function(o, symbol_name))
.transpose()?
.unwrap_or(false)
{
idx = Some(i);
count += 1;
if count > 1 {
break;
}
}
}
2024-08-20 21:40:32 -06:00
match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name),
2024-10-09 21:44:18 -06:00
(1, Some(i)) => &mut project_config.units_mut()[i],
2024-08-20 21:40:32 -06:00
(2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit",
symbol_name
),
_ => unreachable!(),
}
} else {
bail!("Must specify one of: symbol, project and unit, target and base objects")
}
};
let target_path = object.target_path.clone();
let base_path = object.base_path.clone();
(target_path, base_path, Some(project_config))
}
_ => bail!("Either target and base or project and unit must be specified"),
};
if let Some(output) = &args.output {
run_oneshot(&args, output, target_path.as_deref(), base_path.as_deref())
} else {
run_interactive(args, target_path, base_path, project_config)
}
}
2024-12-29 16:57:18 -07:00
fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> {
let mut diff_config = DiffObjConfig::default();
for config in &args.config {
let (key, value) = config.split_once('=').context("--config expects \"key=value\"")?;
let property_id = ConfigPropertyId::from_str(key)
.map_err(|()| anyhow!("Invalid configuration property: {}", key))?;
diff_config.set_property_value_str(property_id, value).map_err(|()| {
let mut options = String::new();
match property_id.kind() {
ConfigPropertyKind::Boolean => {
options = "true, false".to_string();
}
ConfigPropertyKind::Choice(variants) => {
for (i, variant) in variants.iter().enumerate() {
if i > 0 {
options.push_str(", ");
}
options.push_str(variant.value);
}
}
}
anyhow!("Invalid value for {}. Expected one of: {}", property_id.name(), options)
})?;
}
let mut mapping_config = MappingConfig {
mappings: Default::default(),
selecting_left: args.selecting_left.clone(),
selecting_right: args.selecting_right.clone(),
};
for mapping in &args.mapping {
let (target, base) =
mapping.split_once('=').context("--mapping expects \"target=base\"")?;
mapping_config.mappings.insert(target.to_string(), base.to_string());
}
Ok((diff_config, mapping_config))
}
2024-08-20 21:40:32 -06:00
fn run_oneshot(
args: &Args,
output: &Path,
target_path: Option<&Path>,
base_path: Option<&Path>,
) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
2024-12-29 16:57:18 -07:00
let (diff_config, mapping_config) = build_config_from_args(args)?;
2024-08-20 21:40:32 -06:00
let target = target_path
2024-12-29 16:57:18 -07:00
.map(|p| {
obj::read::read(p, &diff_config).with_context(|| format!("Loading {}", p.display()))
})
2024-08-20 21:40:32 -06:00
.transpose()?;
let base = base_path
2024-12-29 16:57:18 -07:00
.map(|p| {
obj::read::read(p, &diff_config).with_context(|| format!("Loading {}", p.display()))
})
2024-08-20 21:40:32 -06:00
.transpose()?;
2024-12-29 16:57:18 -07:00
let result =
diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?;
2024-08-20 21:40:32 -06:00
let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d)));
write_output(&DiffResult::new(left, right), Some(output), output_format)?;
Ok(())
}
2024-10-11 18:37:14 -06:00
pub struct AppState {
pub jobs: JobQueue,
pub waker: Arc<TermWaker>,
pub project_dir: Option<PathBuf>,
pub project_config: Option<ProjectConfig>,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub left_obj: Option<(ObjInfo, ObjDiff)>,
pub right_obj: Option<(ObjInfo, ObjDiff)>,
pub prev_obj: Option<(ObjInfo, ObjDiff)>,
pub reload_time: Option<time::OffsetDateTime>,
pub time_format: Vec<time::format_description::FormatItem<'static>>,
pub watcher: Option<Watcher>,
pub modified: Arc<AtomicBool>,
2024-12-29 16:57:18 -07:00
pub diff_obj_config: DiffObjConfig,
pub mapping_config: MappingConfig,
2024-10-11 18:37:14 -06:00
}
fn create_objdiff_config(state: &AppState) -> ObjDiffConfig {
ObjDiffConfig {
build_config: BuildConfig {
project_dir: state.project_dir.clone(),
custom_make: state
.project_config
.as_ref()
.and_then(|c| c.custom_make.as_ref())
.cloned(),
custom_args: state
.project_config
.as_ref()
.and_then(|c| c.custom_args.as_ref())
.cloned(),
selected_wsl_distro: None,
},
build_base: state.project_config.as_ref().is_some_and(|p| p.build_base.unwrap_or(true)),
build_target: state
.project_config
.as_ref()
.is_some_and(|p| p.build_target.unwrap_or(false)),
target_path: state.target_path.clone(),
base_path: state.base_path.clone(),
2024-12-29 16:57:18 -07:00
diff_obj_config: state.diff_obj_config.clone(),
mapping_config: state.mapping_config.clone(),
2024-10-11 18:37:14 -06:00
}
}
impl AppState {
fn reload(&mut self) -> Result<()> {
let config = create_objdiff_config(self);
self.jobs.push_once(Job::ObjDiff, || start_build(Waker::from(self.waker.clone()), config));
Ok(())
}
fn check_jobs(&mut self) -> Result<bool> {
let mut redraw = false;
self.jobs.collect_results();
for result in mem::take(&mut self.jobs.results) {
match result {
JobResult::None => unreachable!("Unexpected JobResult::None"),
JobResult::ObjDiff(result) => {
let result = result.unwrap();
self.left_obj = result.first_obj;
self.right_obj = result.second_obj;
self.reload_time = Some(result.time);
redraw = true;
}
JobResult::CheckUpdate(_) => todo!("CheckUpdate"),
JobResult::Update(_) => todo!("Update"),
JobResult::CreateScratch(_) => todo!("CreateScratch"),
}
}
Ok(redraw)
}
}
#[derive(Default)]
pub struct TermWaker(pub AtomicBool);
impl Wake for TermWaker {
fn wake(self: Arc<Self>) { self.0.store(true, Ordering::Relaxed); }
fn wake_by_ref(self: &Arc<Self>) { self.0.store(true, Ordering::Relaxed); }
}
2024-08-20 21:40:32 -06:00
fn run_interactive(
args: Args,
target_path: Option<PathBuf>,
base_path: Option<PathBuf>,
project_config: Option<ProjectConfig>,
) -> Result<()> {
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
.context("Failed to parse time format")?;
2024-12-29 16:57:18 -07:00
let (diff_obj_config, mapping_config) = build_config_from_args(&args)?;
2024-10-11 18:37:14 -06:00
let mut state = AppState {
jobs: Default::default(),
waker: Default::default(),
project_dir: args.project.clone(),
project_config,
target_path,
base_path,
left_obj: None,
right_obj: None,
prev_obj: None,
reload_time: None,
time_format,
2024-10-11 18:37:14 -06:00
watcher: None,
modified: Default::default(),
2024-12-29 16:57:18 -07:00
diff_obj_config,
mapping_config,
2024-10-11 18:37:14 -06:00
};
2024-12-29 16:57:18 -07:00
if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) {
let watch_patterns = project_config.build_watch_patterns()?;
2024-10-11 18:37:14 -06:00
state.watcher = Some(create_watcher(
state.modified.clone(),
project_dir,
build_globset(&watch_patterns)?,
Waker::from(state.waker.clone()),
)?);
}
let mut view: Box<dyn UiView> =
Box::new(FunctionDiffUi { symbol_name: symbol_name.clone(), ..Default::default() });
state.reload()?;
2024-02-27 18:47:51 -07:00
crossterm_panic_handler();
enable_raw_mode()?;
crossterm::queue!(
stdout(),
EnterAlternateScreen,
EnableMouseCapture,
2024-08-20 21:40:32 -06:00
SetTitle(format!("{} - objdiff", symbol_name)),
2024-02-27 18:47:51 -07:00
)?;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
2024-02-27 18:47:51 -07:00
2024-10-11 18:37:14 -06:00
let mut result = EventResult { redraw: true, ..Default::default() };
'outer: loop {
2024-10-11 18:37:14 -06:00
if result.redraw {
terminal.draw(|f| loop {
result.redraw = false;
view.draw(&state, f, &mut result);
result.click_xy = None;
if !result.redraw {
break;
}
// Clear buffer on redraw
f.buffer_mut().reset();
})?;
}
loop {
2024-10-11 18:37:14 -06:00
if event::poll(Duration::from_millis(100))? {
match view.handle_event(&mut state, event::read()?) {
EventControlFlow::Break => break 'outer,
EventControlFlow::Continue(r) => result = r,
EventControlFlow::Reload => {
state.reload()?;
result.redraw = true;
}
2024-10-11 18:37:14 -06:00
}
break;
} else if state.waker.0.swap(false, Ordering::Relaxed) {
if state.modified.swap(false, Ordering::Relaxed) {
state.reload()?;
}
result.redraw = true;
break;
2024-02-27 18:47:51 -07:00
}
}
2024-10-11 18:37:14 -06:00
if state.check_jobs()? {
result.redraw = true;
view.reload(&state)?;
}
2024-02-27 18:47:51 -07:00
}
// Reset terminal
disable_raw_mode()?;
crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?;
2024-02-27 18:47:51 -07:00
Ok(())
}