mirror of
https://github.com/encounter/objdiff.git
synced 2025-06-07 15:13:47 +00:00
454 lines
16 KiB
Rust
454 lines
16 KiB
Rust
use std::{
|
|
io::stdout,
|
|
mem,
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicBool, Ordering},
|
|
},
|
|
task::{Wake, Waker},
|
|
time::Duration,
|
|
};
|
|
|
|
use anyhow::{Context, Result, anyhow, bail};
|
|
use argp::FromArgs;
|
|
use crossterm::{
|
|
event,
|
|
event::{DisableMouseCapture, EnableMouseCapture},
|
|
terminal::{
|
|
EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode,
|
|
},
|
|
};
|
|
use objdiff_core::{
|
|
bindings::diff::DiffResult,
|
|
build::{
|
|
BuildConfig, BuildStatus,
|
|
watcher::{Watcher, create_watcher},
|
|
},
|
|
config::{
|
|
ProjectConfig, ProjectObject, ProjectObjectMetadata, build_globset,
|
|
path::{check_path_buf, platform_path, platform_path_serde_option},
|
|
},
|
|
diff::{self, DiffObjConfig, MappingConfig, ObjectDiff},
|
|
jobs::{
|
|
Job, JobQueue, JobResult,
|
|
objdiff::{ObjDiffConfig, start_build},
|
|
},
|
|
obj::{self, Object},
|
|
};
|
|
use ratatui::prelude::*;
|
|
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
|
|
|
|
use crate::{
|
|
cmd::apply_config_args,
|
|
util::{
|
|
output::{OutputFormat, write_output},
|
|
term::crossterm_panic_handler,
|
|
},
|
|
views::{EventControlFlow, EventResult, UiView, function_diff::FunctionDiffUi},
|
|
};
|
|
|
|
#[derive(FromArgs, PartialEq, Debug)]
|
|
/// Diff two object files. (Interactive or one-shot mode)
|
|
#[argp(subcommand, name = "diff")]
|
|
pub struct Args {
|
|
#[argp(option, short = '1', from_str_fn(platform_path))]
|
|
/// Target object file
|
|
target: Option<Utf8PlatformPathBuf>,
|
|
#[argp(option, short = '2', from_str_fn(platform_path))]
|
|
/// Base object file
|
|
base: Option<Utf8PlatformPathBuf>,
|
|
#[argp(option, short = 'p', from_str_fn(platform_path))]
|
|
/// Project directory
|
|
project: Option<Utf8PlatformPathBuf>,
|
|
#[argp(option, short = 'u')]
|
|
/// Unit name within project
|
|
unit: Option<String>,
|
|
#[argp(option, short = 'o', from_str_fn(platform_path))]
|
|
/// Output file (one-shot mode) ("-" for stdout)
|
|
output: Option<Utf8PlatformPathBuf>,
|
|
#[argp(option)]
|
|
/// Output format (json, json-pretty, proto) (default: json)
|
|
format: Option<String>,
|
|
#[argp(positional)]
|
|
/// Function symbol to diff
|
|
symbol: Option<String>,
|
|
#[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>,
|
|
}
|
|
|
|
pub fn run(args: Args) -> Result<()> {
|
|
let (target_path, base_path, project_config) =
|
|
match (&args.target, &args.base, &args.project, &args.unit) {
|
|
(Some(_), Some(_), None, None)
|
|
| (Some(_), None, None, None)
|
|
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
|
|
(None, None, p, u) => {
|
|
let project = match p {
|
|
Some(project) => project.clone(),
|
|
_ => check_path_buf(
|
|
std::env::current_dir().context("Failed to get the current directory")?,
|
|
)
|
|
.context("Current directory is not valid UTF-8")?,
|
|
};
|
|
let Some((project_config, project_config_info)) =
|
|
objdiff_core::config::try_project_config(project.as_ref())
|
|
else {
|
|
bail!("Project config not found in {}", &project)
|
|
};
|
|
let project_config = project_config.with_context(|| {
|
|
format!("Reading project config {}", project_config_info.path.display())
|
|
})?;
|
|
let target_obj_dir = project_config
|
|
.target_dir
|
|
.as_ref()
|
|
.map(|p| project.join(p.with_platform_encoding()));
|
|
let base_obj_dir = project_config
|
|
.base_dir
|
|
.as_ref()
|
|
.map(|p| project.join(p.with_platform_encoding()));
|
|
let objects = project_config
|
|
.units
|
|
.iter()
|
|
.flatten()
|
|
.map(|o| {
|
|
ObjectConfig::new(
|
|
o,
|
|
&project,
|
|
target_obj_dir.as_deref(),
|
|
base_obj_dir.as_deref(),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let object = if let Some(u) = u {
|
|
objects
|
|
.iter()
|
|
.find(|obj| obj.name == *u)
|
|
.ok_or_else(|| anyhow!("Unit not found: {}", u))?
|
|
} else if let Some(symbol_name) = &args.symbol {
|
|
let mut idx = None;
|
|
let mut count = 0usize;
|
|
for (i, obj) in objects.iter().enumerate() {
|
|
if obj
|
|
.target_path
|
|
.as_deref()
|
|
.map(|o| obj::read::has_function(o.as_ref(), symbol_name))
|
|
.transpose()?
|
|
.unwrap_or(false)
|
|
{
|
|
idx = Some(i);
|
|
count += 1;
|
|
if count > 1 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
match (count, idx) {
|
|
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
|
(1, Some(i)) => &objects[i],
|
|
(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)
|
|
}
|
|
}
|
|
|
|
fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> {
|
|
let mut diff_config = DiffObjConfig::default();
|
|
apply_config_args(&mut diff_config, &args.config)?;
|
|
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))
|
|
}
|
|
|
|
fn run_oneshot(
|
|
args: &Args,
|
|
output: &Utf8PlatformPath,
|
|
target_path: Option<&Utf8PlatformPath>,
|
|
base_path: Option<&Utf8PlatformPath>,
|
|
) -> Result<()> {
|
|
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
|
let (diff_config, mapping_config) = build_config_from_args(args)?;
|
|
let target = target_path
|
|
.map(|p| {
|
|
obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p))
|
|
})
|
|
.transpose()?;
|
|
let base = base_path
|
|
.map(|p| {
|
|
obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p))
|
|
})
|
|
.transpose()?;
|
|
let result =
|
|
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
|
|
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(())
|
|
}
|
|
|
|
pub struct AppState {
|
|
pub jobs: JobQueue,
|
|
pub waker: Arc<TermWaker>,
|
|
pub project_dir: Option<Utf8PlatformPathBuf>,
|
|
pub project_config: Option<ProjectConfig>,
|
|
pub target_path: Option<Utf8PlatformPathBuf>,
|
|
pub base_path: Option<Utf8PlatformPathBuf>,
|
|
pub left_status: Option<BuildStatus>,
|
|
pub right_status: Option<BuildStatus>,
|
|
pub left_obj: Option<(Object, ObjectDiff)>,
|
|
pub right_obj: Option<(Object, ObjectDiff)>,
|
|
pub prev_obj: Option<(Object, ObjectDiff)>,
|
|
pub reload_time: Option<time::OffsetDateTime>,
|
|
pub time_format: Vec<time::format_description::FormatItem<'static>>,
|
|
pub watcher: Option<Watcher>,
|
|
pub modified: Arc<AtomicBool>,
|
|
pub diff_obj_config: DiffObjConfig,
|
|
pub mapping_config: MappingConfig,
|
|
}
|
|
|
|
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(),
|
|
diff_obj_config: state.diff_obj_config.clone(),
|
|
mapping_config: state.mapping_config.clone(),
|
|
}
|
|
}
|
|
|
|
/// The configuration for a single object file.
|
|
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct ObjectConfig {
|
|
pub name: String,
|
|
#[serde(default, with = "platform_path_serde_option")]
|
|
pub target_path: Option<Utf8PlatformPathBuf>,
|
|
#[serde(default, with = "platform_path_serde_option")]
|
|
pub base_path: Option<Utf8PlatformPathBuf>,
|
|
pub metadata: ProjectObjectMetadata,
|
|
pub complete: Option<bool>,
|
|
}
|
|
|
|
impl ObjectConfig {
|
|
pub fn new(
|
|
object: &ProjectObject,
|
|
project_dir: &Utf8PlatformPath,
|
|
target_obj_dir: Option<&Utf8PlatformPath>,
|
|
base_obj_dir: Option<&Utf8PlatformPath>,
|
|
) -> Self {
|
|
let target_path = if let (Some(target_obj_dir), Some(path), None) =
|
|
(target_obj_dir, &object.path, &object.target_path)
|
|
{
|
|
Some(target_obj_dir.join(path.with_platform_encoding()))
|
|
} else {
|
|
object.target_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
|
|
};
|
|
let base_path = if let (Some(base_obj_dir), Some(path), None) =
|
|
(base_obj_dir, &object.path, &object.base_path)
|
|
{
|
|
Some(base_obj_dir.join(path.with_platform_encoding()))
|
|
} else {
|
|
object.base_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
|
|
};
|
|
Self {
|
|
name: object.name().to_string(),
|
|
target_path,
|
|
base_path,
|
|
metadata: object.metadata.clone().unwrap_or_default(),
|
|
complete: object.complete(),
|
|
}
|
|
}
|
|
}
|
|
|
|
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_status = Some(result.first_status);
|
|
self.right_status = Some(result.second_status);
|
|
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); }
|
|
}
|
|
|
|
fn run_interactive(
|
|
args: Args,
|
|
target_path: Option<Utf8PlatformPathBuf>,
|
|
base_path: Option<Utf8PlatformPathBuf>,
|
|
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")?;
|
|
let (diff_obj_config, mapping_config) = build_config_from_args(&args)?;
|
|
let mut state = AppState {
|
|
jobs: Default::default(),
|
|
waker: Default::default(),
|
|
project_dir: args.project.clone(),
|
|
project_config,
|
|
target_path,
|
|
base_path,
|
|
left_status: None,
|
|
right_status: None,
|
|
left_obj: None,
|
|
right_obj: None,
|
|
prev_obj: None,
|
|
reload_time: None,
|
|
time_format,
|
|
watcher: None,
|
|
modified: Default::default(),
|
|
diff_obj_config,
|
|
mapping_config,
|
|
};
|
|
if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) {
|
|
let watch_patterns = project_config.build_watch_patterns()?;
|
|
state.watcher = Some(create_watcher(
|
|
state.modified.clone(),
|
|
project_dir.as_ref(),
|
|
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()?;
|
|
|
|
crossterm_panic_handler();
|
|
enable_raw_mode()?;
|
|
crossterm::queue!(
|
|
stdout(),
|
|
EnterAlternateScreen,
|
|
EnableMouseCapture,
|
|
SetTitle(format!("{} - objdiff", symbol_name)),
|
|
)?;
|
|
let backend = CrosstermBackend::new(stdout());
|
|
let mut terminal = Terminal::new(backend)?;
|
|
|
|
let mut result = EventResult { redraw: true, ..Default::default() };
|
|
'outer: loop {
|
|
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 {
|
|
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;
|
|
}
|
|
}
|
|
break;
|
|
} else if state.waker.0.swap(false, Ordering::Relaxed) {
|
|
if state.modified.swap(false, Ordering::Relaxed) {
|
|
state.reload()?;
|
|
}
|
|
result.redraw = true;
|
|
break;
|
|
}
|
|
}
|
|
if state.check_jobs()? {
|
|
result.redraw = true;
|
|
view.reload(&state)?;
|
|
}
|
|
}
|
|
|
|
// Reset terminal
|
|
disable_raw_mode()?;
|
|
crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
|
|
terminal.show_cursor()?;
|
|
Ok(())
|
|
}
|