From 5a8e4cb608c80eeca5b44f1d4b8c77d8d6f2bf14 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 11 Oct 2024 18:37:14 -0600 Subject: [PATCH] [WIP] objdiff-cli diff auto-rebuild --- Cargo.lock | 49 +- Cargo.toml | 2 +- objdiff-cli/src/cmd/diff.rs | 900 ++++-------------- objdiff-cli/src/main.rs | 1 + objdiff-cli/src/views/function_diff.rs | 648 +++++++++++++ objdiff-cli/src/views/mod.rs | 25 + objdiff-core/Cargo.toml | 26 +- objdiff-core/src/build/mod.rs | 106 +++ objdiff-core/src/build/watcher.rs | 72 ++ objdiff-core/src/config/mod.rs | 4 + objdiff-core/src/jobs/check_update.rs | 50 + .../src/jobs/create_scratch.rs | 49 +- {objdiff-gui => objdiff-core}/src/jobs/mod.rs | 55 +- objdiff-core/src/jobs/objdiff.rs | 199 ++++ .../src/jobs/update.rs | 38 +- objdiff-core/src/lib.rs | 4 + objdiff-gui/Cargo.toml | 16 - objdiff-gui/src/app.rs | 117 +-- objdiff-gui/src/jobs.rs | 137 +++ objdiff-gui/src/jobs/check_update.rs | 39 - objdiff-gui/src/jobs/objdiff.rs | 328 ------- objdiff-gui/src/update.rs | 8 +- objdiff-gui/src/views/config.rs | 11 +- objdiff-gui/src/views/jobs.rs | 6 +- objdiff-gui/src/views/symbol_diff.rs | 19 +- 25 files changed, 1589 insertions(+), 1320 deletions(-) create mode 100644 objdiff-cli/src/views/function_diff.rs create mode 100644 objdiff-cli/src/views/mod.rs create mode 100644 objdiff-core/src/build/mod.rs create mode 100644 objdiff-core/src/build/watcher.rs create mode 100644 objdiff-core/src/jobs/check_update.rs rename {objdiff-gui => objdiff-core}/src/jobs/create_scratch.rs (64%) rename {objdiff-gui => objdiff-core}/src/jobs/mod.rs (71%) create mode 100644 objdiff-core/src/jobs/objdiff.rs rename {objdiff-gui => objdiff-core}/src/jobs/update.rs (59%) create mode 100644 objdiff-gui/src/jobs.rs delete mode 100644 objdiff-gui/src/jobs/check_update.rs delete mode 100644 objdiff-gui/src/jobs/objdiff.rs diff --git a/Cargo.lock b/Cargo.lock index 5f44186..6cbd033 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1372,6 +1372,14 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "file-id" +version = "0.2.1" +source = "git+https://github.com/notify-rs/notify?rev=128bf6230c03d39dbb7f301ff7b20e594e34c3a2#128bf6230c03d39dbb7f301ff7b20e594e34c3a2" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "filetime" version = "0.2.25" @@ -2557,6 +2565,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "notify-debouncer-full" +version = "0.4.0" +source = "git+https://github.com/notify-rs/notify?rev=128bf6230c03d39dbb7f301ff7b20e594e34c3a2#128bf6230c03d39dbb7f301ff7b20e594e34c3a2" +dependencies = [ + "file-id", + "log", + "notify", + "notify-types", + "walkdir", +] + [[package]] name = "notify-types" version = "1.0.0" @@ -2861,7 +2881,7 @@ dependencies = [ [[package]] name = "objdiff-cli" -version = "2.3.2" +version = "2.4.0" dependencies = [ "anyhow", "argp", @@ -2883,7 +2903,7 @@ dependencies = [ [[package]] name = "objdiff-core" -version = "2.3.2" +version = "2.4.0" dependencies = [ "anyhow", "arm-attr", @@ -2902,28 +2922,37 @@ dependencies = [ "log", "memmap2", "msvc-demangler", + "notify", + "notify-debouncer-full", "num-traits", "object", + "path-slash", "pbjson", "pbjson-build", "ppc750cl", "prost", "prost-build", "rabbitizer", + "reqwest", + "self_update", "semver", "serde", "serde_json", "serde_yaml", + "shell-escape", "similar", "strum", + "tempfile", + "time", "tsify-next", "unarm", "wasm-bindgen", + "winapi", ] [[package]] name = "objdiff-gui" -version = "2.3.2" +version = "2.4.0" dependencies = [ "anyhow", "bytes", @@ -2942,29 +2971,23 @@ dependencies = [ "font-kit", "globset", "log", - "notify", "objdiff-core", "open", - "path-slash", "png", "pollster", "regex", - "reqwest", "rfd", "rlwinmdec", "ron", - "self_update", "serde", "serde_json", "shell-escape", "strum", "tauri-winres", - "tempfile", "time", "tracing-subscriber", "tracing-wasm", "wgpu", - "winapi", ] [[package]] @@ -3804,9 +3827,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "once_cell", "ring", @@ -3865,9 +3888,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ "windows-sys 0.59.0", ] diff --git a/Cargo.toml b/Cargo.toml index faae7dc..bd97d80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ strip = "debuginfo" codegen-units = 1 [workspace.package] -version = "2.3.2" +version = "2.4.0" authors = ["Luke Street "] edition = "2021" license = "MIT OR Apache-2.0" diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index c24dff4..92af5a1 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -1,42 +1,50 @@ use std::{ fs, io::stdout, + mem, path::{Path, PathBuf}, str::FromStr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + task::{Wake, Waker}, + time::Duration, }; use anyhow::{bail, Context, Result}; use argp::FromArgs; use crossterm::{ event, - event::{ - DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton, - MouseEventKind, - }, + event::{DisableMouseCapture, EnableMouseCapture}, terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle, }, }; -use event::KeyModifiers; use objdiff_core::{ bindings::diff::DiffResult, - config::{ProjectConfig, ProjectObject}, + build::{ + watcher::{create_watcher, Watcher}, + BuildConfig, + }, + config::{build_globset, default_watch_patterns, ProjectConfig, ProjectObject}, diff, - diff::{ - display::{display_diff, DiffText, HighlightKind}, - DiffObjsResult, ObjDiff, ObjInsDiffKind, ObjSymbolDiff, + diff::ObjDiff, + jobs::{ + objdiff::{start_build, ObjDiffConfig}, + Job, JobQueue, JobResult, }, obj, - obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef}, -}; -use ratatui::{ - prelude::*, - widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}, + obj::ObjInfo, }; +use ratatui::prelude::*; -use crate::util::{ - output::{write_output, OutputFormat}, - term::crossterm_panic_handler, +use crate::{ + util::{ + output::{write_output, OutputFormat}, + term::crossterm_panic_handler, + }, + views::{function_diff::FunctionDiffUi, EventControlFlow, EventResult, UiView}, }; #[derive(FromArgs, PartialEq, Debug)] @@ -209,6 +217,94 @@ fn run_oneshot( Ok(()) } +pub struct AppState { + pub jobs: JobQueue, + pub waker: Arc, + pub project_dir: Option, + pub project_config: Option, + pub target_path: Option, + pub base_path: Option, + pub left_obj: Option<(ObjInfo, ObjDiff)>, + pub right_obj: Option<(ObjInfo, ObjDiff)>, + pub prev_obj: Option<(ObjInfo, ObjDiff)>, + pub reload_time: Option, + pub time_format: Vec>, + pub relax_reloc_diffs: bool, + pub watcher: Option, + pub modified: Arc, +} + +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().map_or(false, |p| p.build_base.unwrap_or(true)), + build_target: state + .project_config + .as_ref() + .map_or(false, |p| p.build_target.unwrap_or(false)), + target_path: state.target_path.clone(), + base_path: state.base_path.clone(), + diff_obj_config: diff::DiffObjConfig { + relax_reloc_diffs: state.relax_reloc_diffs, + ..Default::default() // TODO + }, + symbol_mappings: Default::default(), + selecting_left: None, + selecting_right: None, + } +} + +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 { + 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.0.store(true, Ordering::Relaxed); } + + fn wake_by_ref(self: &Arc) { self.0.store(true, Ordering::Relaxed); } +} + fn run_interactive( args: Args, target_path: Option, @@ -218,32 +314,38 @@ fn run_interactive( 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 mut state = Box::new(FunctionDiffUi { - relax_reloc_diffs: args.relax_reloc_diffs, - left_highlight: HighlightKind::None, - right_highlight: HighlightKind::None, - scroll_x: 0, - scroll_state_x: ScrollbarState::default(), - scroll_y: 0, - scroll_state_y: ScrollbarState::default(), - per_page: 0, - num_rows: 0, - symbol_name: symbol_name.clone(), + let mut state = AppState { + jobs: Default::default(), + waker: Default::default(), + project_dir: args.project.clone(), + project_config, target_path, base_path, - project_config, left_obj: None, right_obj: None, prev_obj: None, - diff_result: DiffObjsResult::default(), - left_sym: None, - right_sym: None, - prev_sym: None, reload_time: None, time_format, - open_options: false, - three_way: false, - }); + relax_reloc_diffs: args.relax_reloc_diffs, + watcher: None, + modified: Default::default(), + }; + if let Some(project_dir) = &state.project_dir { + let watch_patterns = state + .project_config + .as_ref() + .and_then(|c| c.watch_patterns.as_ref()) + .cloned() + .unwrap_or_else(default_watch_patterns); + state.watcher = Some(create_watcher( + state.modified.clone(), + project_dir, + build_globset(&watch_patterns)?, + Waker::from(state.waker.clone()), + )?); + } + let mut view: Box = + Box::new(FunctionDiffUi { symbol_name: symbol_name.clone(), ..Default::default() }); state.reload()?; crossterm_panic_handler(); @@ -257,31 +359,44 @@ fn run_interactive( let backend = CrosstermBackend::new(stdout()); let mut terminal = Terminal::new(backend)?; + let mut result = EventResult { redraw: true, ..Default::default() }; 'outer: loop { - let mut result = EventResult { redraw: true, ..Default::default() }; + 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 result.redraw { - terminal.draw(|f| loop { - result.redraw = false; - state.draw(f, &mut result); - if state.open_options { - state.draw_options(f, &mut result); + let has_event = event::poll(Duration::from_millis(100))?; + if has_event { + match view.handle_event(&mut state, event::read()?) { + EventControlFlow::Break => break 'outer, + EventControlFlow::Continue(r) => result = r, + EventControlFlow::Reload => { + state.reload()?; + result.redraw = true; } - result.click_xy = None; - if !result.redraw { - break; - } - // Clear buffer on redraw - f.buffer_mut().reset(); - })?; - } - match state.handle_event(event::read()?) { - EventControlFlow::Break => break 'outer, - EventControlFlow::Continue(r) => result = r, - EventControlFlow::Reload => break, + } + break; + } else if state.waker.0.swap(false, Ordering::Relaxed) { + if state.modified.swap(false, Ordering::Relaxed) { + state.reload()?; + } + result.redraw = true; + break; } } - state.reload()?; + if state.check_jobs()? { + result.redraw = true; + view.reload(&state)?; + } } // Reset terminal @@ -290,676 +405,3 @@ fn run_interactive( terminal.show_cursor()?; Ok(()) } - -#[inline] -fn get_symbol(obj: Option<&ObjInfo>, sym: Option) -> Option<&ObjSymbol> { - Some(obj?.section_symbol(sym?).1) -} - -#[inline] -fn get_symbol_diff(obj: Option<&ObjDiff>, sym: Option) -> Option<&ObjSymbolDiff> { - Some(obj?.symbol_diff(sym?)) -} - -fn find_function(obj: &ObjInfo, name: &str) -> Option { - for (section_idx, section) in obj.sections.iter().enumerate() { - if section.kind != ObjSectionKind::Code { - continue; - } - for (symbol_idx, symbol) in section.symbols.iter().enumerate() { - if symbol.name == name { - return Some(SymbolRef { section_idx, symbol_idx }); - } - } - } - None -} - -#[expect(dead_code)] -struct FunctionDiffUi { - relax_reloc_diffs: bool, - left_highlight: HighlightKind, - right_highlight: HighlightKind, - scroll_x: usize, - scroll_state_x: ScrollbarState, - scroll_y: usize, - scroll_state_y: ScrollbarState, - per_page: usize, - num_rows: usize, - symbol_name: String, - target_path: Option, - base_path: Option, - project_config: Option, - left_obj: Option, - right_obj: Option, - prev_obj: Option, - diff_result: DiffObjsResult, - left_sym: Option, - right_sym: Option, - prev_sym: Option, - reload_time: Option, - time_format: Vec>, - open_options: bool, - three_way: bool, -} - -#[derive(Default)] -struct EventResult { - redraw: bool, - click_xy: Option<(u16, u16)>, -} - -enum EventControlFlow { - Break, - Continue(EventResult), - Reload, -} - -impl FunctionDiffUi { - fn draw(&mut self, f: &mut Frame, result: &mut EventResult) { - let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area()); - let header_chunks = Layout::horizontal([ - Constraint::Fill(1), - Constraint::Length(3), - Constraint::Fill(1), - Constraint::Length(2), - ]) - .split(chunks[0]); - let content_chunks = if self.three_way { - Layout::horizontal([ - Constraint::Fill(1), - Constraint::Length(3), - Constraint::Fill(1), - Constraint::Length(3), - Constraint::Fill(1), - Constraint::Length(2), - ]) - .split(chunks[1]) - } else { - Layout::horizontal([ - Constraint::Fill(1), - Constraint::Length(3), - Constraint::Fill(1), - Constraint::Length(2), - ]) - .split(chunks[1]) - }; - - self.per_page = chunks[1].height.saturating_sub(2) as usize; - let max_scroll_y = self.num_rows.saturating_sub(self.per_page); - if self.scroll_y > max_scroll_y { - self.scroll_y = max_scroll_y; - } - self.scroll_state_y = - self.scroll_state_y.content_length(max_scroll_y).position(self.scroll_y); - - let mut line_l = Line::default(); - line_l - .spans - .push(Span::styled(self.symbol_name.clone(), Style::new().fg(Color::White).bold())); - f.render_widget(line_l, header_chunks[0]); - - let mut line_r = Line::default(); - if let Some(percent) = get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym) - .and_then(|s| s.match_percent) - { - line_r.spans.push(Span::styled( - format!("{:.2}% ", percent), - Style::new().fg(match_percent_color(percent)), - )); - } - let reload_time = self - .reload_time - .as_ref() - .and_then(|t| t.format(&self.time_format).ok()) - .unwrap_or_else(|| "N/A".to_string()); - line_r.spans.push(Span::styled( - format!("Last reload: {}", reload_time), - Style::new().fg(Color::White), - )); - f.render_widget(line_r, header_chunks[2]); - - let mut left_text = None; - let mut left_highlight = None; - let mut max_width = 0; - if let (Some(symbol), Some(symbol_diff)) = ( - get_symbol(self.left_obj.as_ref(), self.left_sym), - get_symbol_diff(self.diff_result.left.as_ref(), self.left_sym), - ) { - let mut text = Text::default(); - let rect = content_chunks[0].inner(Margin::new(0, 1)); - left_highlight = self.print_sym( - &mut text, - symbol, - symbol_diff, - rect, - &self.left_highlight, - result, - false, - ); - max_width = max_width.max(text.width()); - left_text = Some(text); - } - - let mut right_text = None; - let mut right_highlight = None; - let mut margin_text = None; - if let (Some(symbol), Some(symbol_diff)) = ( - get_symbol(self.right_obj.as_ref(), self.right_sym), - get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym), - ) { - let mut text = Text::default(); - let rect = content_chunks[2].inner(Margin::new(0, 1)); - right_highlight = self.print_sym( - &mut text, - symbol, - symbol_diff, - rect, - &self.right_highlight, - result, - false, - ); - max_width = max_width.max(text.width()); - right_text = Some(text); - - // Render margin - let mut text = Text::default(); - let rect = content_chunks[1].inner(Margin::new(1, 1)); - self.print_margin(&mut text, symbol_diff, rect); - margin_text = Some(text); - } - - let mut prev_text = None; - let mut prev_margin_text = None; - if self.three_way { - if let (Some(symbol), Some(symbol_diff)) = ( - get_symbol(self.prev_obj.as_ref(), self.prev_sym), - get_symbol_diff(self.diff_result.prev.as_ref(), self.prev_sym), - ) { - let mut text = Text::default(); - let rect = content_chunks[4].inner(Margin::new(0, 1)); - self.print_sym( - &mut text, - symbol, - symbol_diff, - rect, - &self.right_highlight, - result, - true, - ); - max_width = max_width.max(text.width()); - prev_text = Some(text); - - // Render margin - let mut text = Text::default(); - let rect = content_chunks[3].inner(Margin::new(1, 1)); - self.print_margin(&mut text, symbol_diff, rect); - prev_margin_text = Some(text); - } - } - - let max_scroll_x = - max_width.saturating_sub(content_chunks[0].width.min(content_chunks[2].width) as usize); - if self.scroll_x > max_scroll_x { - self.scroll_x = max_scroll_x; - } - self.scroll_state_x = - self.scroll_state_x.content_length(max_scroll_x).position(self.scroll_x); - - if let Some(text) = left_text { - // Render left column - f.render_widget( - Paragraph::new(text) - .block( - Block::new() - .borders(Borders::TOP) - .border_style(Style::new().fg(Color::Gray)) - .title_style(Style::new().bold()) - .title("TARGET"), - ) - .scroll((0, self.scroll_x as u16)), - content_chunks[0], - ); - } - if let Some(text) = margin_text { - f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1))); - } - if let Some(text) = right_text { - f.render_widget( - Paragraph::new(text) - .block( - Block::new() - .borders(Borders::TOP) - .border_style(Style::new().fg(Color::Gray)) - .title_style(Style::new().bold()) - .title("CURRENT"), - ) - .scroll((0, self.scroll_x as u16)), - content_chunks[2], - ); - } - - if self.three_way { - if let Some(text) = prev_margin_text { - f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1))); - } - let block = Block::new() - .borders(Borders::TOP) - .border_style(Style::new().fg(Color::Gray)) - .title_style(Style::new().bold()) - .title("SAVED"); - if let Some(text) = prev_text { - f.render_widget( - Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)), - content_chunks[4], - ); - } else { - f.render_widget(block, content_chunks[4]); - } - } - - // Render scrollbars - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None), - chunks[1].inner(Margin::new(0, 1)), - &mut self.scroll_state_y, - ); - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"), - content_chunks[0], - &mut self.scroll_state_x, - ); - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"), - content_chunks[2], - &mut self.scroll_state_x, - ); - if self.three_way { - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"), - content_chunks[4], - &mut self.scroll_state_x, - ); - } - - if let Some(new_highlight) = left_highlight { - if new_highlight == self.left_highlight { - if self.left_highlight != self.right_highlight { - self.right_highlight = self.left_highlight.clone(); - } else { - self.left_highlight = HighlightKind::None; - self.right_highlight = HighlightKind::None; - } - } else { - self.left_highlight = new_highlight; - } - result.redraw = true; - } else if let Some(new_highlight) = right_highlight { - if new_highlight == self.right_highlight { - if self.left_highlight != self.right_highlight { - self.left_highlight = self.right_highlight.clone(); - } else { - self.left_highlight = HighlightKind::None; - self.right_highlight = HighlightKind::None; - } - } else { - self.right_highlight = new_highlight; - } - result.redraw = true; - } - } - - fn draw_options(&mut self, f: &mut Frame, _result: &mut EventResult) { - let percent_x = 50; - let percent_y = 50; - let popup_rect = Layout::vertical([ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ]) - .split(f.area())[1]; - let popup_rect = Layout::horizontal([ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ]) - .split(popup_rect)[1]; - - let popup = Block::default() - .borders(Borders::ALL) - .title("Options") - .title_style(Style::default().fg(Color::White).bg(Color::Black)); - f.render_widget(Clear, popup_rect); - f.render_widget(popup, popup_rect); - } - - fn handle_event(&mut self, event: Event) -> EventControlFlow { - let mut result = EventResult::default(); - match event { - Event::Key(event) - if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) => - { - match event.code { - // Quit - KeyCode::Esc | KeyCode::Char('q') => return EventControlFlow::Break, - // Page up - KeyCode::PageUp => { - self.page_up(false); - result.redraw = true; - } - // Page up (shift + space) - KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => { - self.page_up(false); - result.redraw = true; - } - // Page down - KeyCode::Char(' ') | KeyCode::PageDown => { - self.page_down(false); - result.redraw = true; - } - // Page down (ctrl + f) - KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => { - self.page_down(false); - result.redraw = true; - } - // Page up (ctrl + b) - KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => { - self.page_up(false); - result.redraw = true; - } - // Half page down (ctrl + d) - KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => { - self.page_down(true); - result.redraw = true; - } - // Half page up (ctrl + u) - KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => { - self.page_up(true); - result.redraw = true; - } - // Scroll down - KeyCode::Down | KeyCode::Char('j') => { - self.scroll_y += 1; - result.redraw = true; - } - // Scroll up - KeyCode::Up | KeyCode::Char('k') => { - self.scroll_y = self.scroll_y.saturating_sub(1); - result.redraw = true; - } - // Scroll to start - KeyCode::Char('g') => { - self.scroll_y = 0; - result.redraw = true; - } - // Scroll to end - KeyCode::Char('G') => { - self.scroll_y = self.num_rows; - result.redraw = true; - } - // Reload - KeyCode::Char('r') => { - result.redraw = true; - return EventControlFlow::Reload; - } - // Scroll right - KeyCode::Right | KeyCode::Char('l') => { - self.scroll_x += 1; - result.redraw = true; - } - // Scroll left - KeyCode::Left | KeyCode::Char('h') => { - self.scroll_x = self.scroll_x.saturating_sub(1); - result.redraw = true; - } - // Toggle relax relocation diffs - KeyCode::Char('x') => { - self.relax_reloc_diffs = !self.relax_reloc_diffs; - result.redraw = true; - return EventControlFlow::Reload; - } - // Toggle three-way diff - KeyCode::Char('3') => { - self.three_way = !self.three_way; - result.redraw = true; - } - // Toggle options - KeyCode::Char('o') => { - self.open_options = !self.open_options; - result.redraw = true; - } - _ => {} - } - } - Event::Mouse(event) => match event.kind { - MouseEventKind::ScrollDown => { - self.scroll_y += 3; - result.redraw = true; - } - MouseEventKind::ScrollUp => { - self.scroll_y = self.scroll_y.saturating_sub(3); - result.redraw = true; - } - MouseEventKind::ScrollRight => { - self.scroll_x += 3; - result.redraw = true; - } - MouseEventKind::ScrollLeft => { - self.scroll_x = self.scroll_x.saturating_sub(3); - result.redraw = true; - } - MouseEventKind::Down(MouseButton::Left) => { - result.click_xy = Some((event.column, event.row)); - result.redraw = true; - } - _ => {} - }, - Event::Resize(_, _) => { - result.redraw = true; - } - _ => {} - } - EventControlFlow::Continue(result) - } - - fn page_up(&mut self, half: bool) { - self.scroll_y = self.scroll_y.saturating_sub(self.per_page / if half { 2 } else { 1 }); - } - - fn page_down(&mut self, half: bool) { - self.scroll_y += self.per_page / if half { 2 } else { 1 }; - } - - #[expect(clippy::too_many_arguments)] - fn print_sym( - &self, - out: &mut Text<'static>, - symbol: &ObjSymbol, - symbol_diff: &ObjSymbolDiff, - rect: Rect, - highlight: &HighlightKind, - result: &EventResult, - only_changed: bool, - ) -> Option { - let base_addr = symbol.address; - let mut new_highlight = None; - for (y, ins_diff) in symbol_diff - .instructions - .iter() - .skip(self.scroll_y) - .take(rect.height as usize) - .enumerate() - { - if only_changed && ins_diff.kind == ObjInsDiffKind::None { - out.lines.push(Line::default()); - continue; - } - let mut sx = rect.x; - let sy = rect.y + y as u16; - let mut line = Line::default(); - display_diff(ins_diff, base_addr, |text| -> Result<()> { - let label_text; - let mut base_color = match ins_diff.kind { - ObjInsDiffKind::None - | ObjInsDiffKind::OpMismatch - | ObjInsDiffKind::ArgMismatch => Color::Gray, - ObjInsDiffKind::Replace => Color::Cyan, - ObjInsDiffKind::Delete => Color::Red, - ObjInsDiffKind::Insert => Color::Green, - }; - let mut pad_to = 0; - match text { - DiffText::Basic(text) => { - label_text = text.to_string(); - } - DiffText::BasicColor(s, idx) => { - label_text = s.to_string(); - base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()]; - } - DiffText::Line(num) => { - label_text = format!("{num} "); - base_color = Color::DarkGray; - pad_to = 5; - } - DiffText::Address(addr) => { - label_text = format!("{:x}:", addr); - pad_to = 5; - } - DiffText::Opcode(mnemonic, _op) => { - label_text = mnemonic.to_string(); - if ins_diff.kind == ObjInsDiffKind::OpMismatch { - base_color = Color::Blue; - } - pad_to = 8; - } - DiffText::Argument(arg, diff) => { - label_text = arg.to_string(); - if let Some(diff) = diff { - base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()] - } - } - DiffText::BranchDest(addr, diff) => { - label_text = format!("{addr:x}"); - if let Some(diff) = diff { - base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()] - } - } - DiffText::Symbol(sym) => { - let name = sym.demangled_name.as_ref().unwrap_or(&sym.name); - label_text = name.clone(); - base_color = Color::White; - } - DiffText::Spacing(n) => { - line.spans.push(Span::raw(" ".repeat(n))); - sx += n as u16; - return Ok(()); - } - DiffText::Eol => { - return Ok(()); - } - } - let len = label_text.len(); - let highlighted = *highlight == text; - if let Some((cx, cy)) = result.click_xy { - if cx >= sx && cx < sx + len as u16 && cy == sy { - new_highlight = Some(text.into()); - } - } - let mut style = Style::new().fg(base_color); - if highlighted { - style = style.bg(Color::DarkGray); - } - line.spans.push(Span::styled(label_text, style)); - sx += len as u16; - if pad_to > len { - let pad = (pad_to - len) as u16; - line.spans.push(Span::raw(" ".repeat(pad as usize))); - sx += pad; - } - Ok(()) - }) - .unwrap(); - out.lines.push(line); - } - new_highlight - } - - fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) { - for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) { - if ins_diff.kind != ObjInsDiffKind::None { - out.lines.push(Line::raw(match ins_diff.kind { - ObjInsDiffKind::Delete => "<", - ObjInsDiffKind::Insert => ">", - _ => "|", - })); - } else { - out.lines.push(Line::raw(" ")); - } - } - } - - fn reload(&mut self) -> Result<()> { - let prev = self.right_obj.take(); - let config = diff::DiffObjConfig { - relax_reloc_diffs: self.relax_reloc_diffs, - ..Default::default() // TODO - }; - let target = self - .target_path - .as_deref() - .map(|p| { - obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())) - }) - .transpose()?; - let base = self - .base_path - .as_deref() - .map(|p| { - obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())) - }) - .transpose()?; - let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), prev.as_ref())?; - - let left_sym = target.as_ref().and_then(|o| find_function(o, &self.symbol_name)); - let right_sym = base.as_ref().and_then(|o| find_function(o, &self.symbol_name)); - let prev_sym = prev.as_ref().and_then(|o| find_function(o, &self.symbol_name)); - self.num_rows = match ( - get_symbol_diff(result.left.as_ref(), left_sym), - get_symbol_diff(result.right.as_ref(), right_sym), - ) { - (Some(l), Some(r)) => l.instructions.len().max(r.instructions.len()), - (Some(l), None) => l.instructions.len(), - (None, Some(r)) => r.instructions.len(), - (None, None) => bail!("Symbol not found: {}", self.symbol_name), - }; - self.left_obj = target; - self.right_obj = base; - self.prev_obj = prev; - self.diff_result = result; - self.left_sym = left_sym; - self.right_sym = right_sym; - self.prev_sym = prev_sym; - self.reload_time = time::OffsetDateTime::now_local().ok(); - Ok(()) - } -} - -pub const COLOR_ROTATION: [Color; 7] = [ - Color::Magenta, - Color::Cyan, - Color::Green, - Color::Red, - Color::Yellow, - Color::Blue, - Color::Green, -]; - -pub fn match_percent_color(match_percent: f32) -> Color { - if match_percent == 100.0 { - Color::Green - } else if match_percent >= 50.0 { - Color::LightBlue - } else { - Color::LightRed - } -} diff --git a/objdiff-cli/src/main.rs b/objdiff-cli/src/main.rs index 7485419..e0e35f9 100644 --- a/objdiff-cli/src/main.rs +++ b/objdiff-cli/src/main.rs @@ -1,6 +1,7 @@ mod argp_version; mod cmd; mod util; +mod views; // musl's allocator is very slow, so use mimalloc when targeting musl. // Otherwise, use the system allocator to avoid extra code size. diff --git a/objdiff-cli/src/views/function_diff.rs b/objdiff-cli/src/views/function_diff.rs new file mode 100644 index 0000000..8c7cf55 --- /dev/null +++ b/objdiff-cli/src/views/function_diff.rs @@ -0,0 +1,648 @@ +use anyhow::{bail, Result}; +use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind}; +use objdiff_core::{ + diff::{ + display::{display_diff, DiffText, HighlightKind}, + ObjDiff, ObjInsDiffKind, ObjSymbolDiff, + }, + obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef}, +}; +use ratatui::{ + prelude::*, + widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}, + Frame, +}; + +use super::{EventControlFlow, EventResult, UiView}; +use crate::cmd::diff::AppState; + +#[allow(dead_code)] +#[derive(Default)] +pub struct FunctionDiffUi { + pub symbol_name: String, + pub left_highlight: HighlightKind, + pub right_highlight: HighlightKind, + pub scroll_x: usize, + pub scroll_state_x: ScrollbarState, + pub scroll_y: usize, + pub scroll_state_y: ScrollbarState, + pub per_page: usize, + pub num_rows: usize, + pub left_sym: Option, + pub right_sym: Option, + pub prev_sym: Option, + pub open_options: bool, + pub three_way: bool, +} + +impl UiView for FunctionDiffUi { + fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult) { + let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area()); + let header_chunks = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(2), + ]) + .split(chunks[0]); + let content_chunks = if self.three_way { + Layout::horizontal([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(2), + ]) + .split(chunks[1]) + } else { + Layout::horizontal([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(2), + ]) + .split(chunks[1]) + }; + + self.per_page = chunks[1].height.saturating_sub(2) as usize; + let max_scroll_y = self.num_rows.saturating_sub(self.per_page); + if self.scroll_y > max_scroll_y { + self.scroll_y = max_scroll_y; + } + self.scroll_state_y = + self.scroll_state_y.content_length(max_scroll_y).position(self.scroll_y); + + let mut line_l = Line::default(); + line_l + .spans + .push(Span::styled(self.symbol_name.clone(), Style::new().fg(Color::White).bold())); + f.render_widget(line_l, header_chunks[0]); + + let mut line_r = Line::default(); + if let Some(percent) = + get_symbol(state.right_obj.as_ref(), self.right_sym).and_then(|(_, d)| d.match_percent) + { + line_r.spans.push(Span::styled( + format!("{:.2}% ", percent), + Style::new().fg(match_percent_color(percent)), + )); + } + let reload_time = state + .reload_time + .as_ref() + .and_then(|t| t.format(&state.time_format).ok()) + .unwrap_or_else(|| "N/A".to_string()); + line_r.spans.push(Span::styled( + format!("Last reload: {}", reload_time), + Style::new().fg(Color::White), + )); + line_r.spans.push(Span::styled( + format!(" ({} jobs)", state.jobs.jobs.len()), + Style::new().fg(Color::LightYellow), + )); + f.render_widget(line_r, header_chunks[2]); + + let mut left_text = None; + let mut left_highlight = None; + let mut max_width = 0; + if let Some((symbol, symbol_diff)) = get_symbol(state.left_obj.as_ref(), self.left_sym) { + let mut text = Text::default(); + let rect = content_chunks[0].inner(Margin::new(0, 1)); + left_highlight = self.print_sym( + &mut text, + symbol, + symbol_diff, + rect, + &self.left_highlight, + result, + false, + ); + max_width = max_width.max(text.width()); + left_text = Some(text); + } + + let mut right_text = None; + let mut right_highlight = None; + let mut margin_text = None; + if let Some((symbol, symbol_diff)) = get_symbol(state.right_obj.as_ref(), self.right_sym) { + let mut text = Text::default(); + let rect = content_chunks[2].inner(Margin::new(0, 1)); + right_highlight = self.print_sym( + &mut text, + symbol, + symbol_diff, + rect, + &self.right_highlight, + result, + false, + ); + max_width = max_width.max(text.width()); + right_text = Some(text); + + // Render margin + let mut text = Text::default(); + let rect = content_chunks[1].inner(Margin::new(1, 1)); + self.print_margin(&mut text, symbol_diff, rect); + margin_text = Some(text); + } + + let mut prev_text = None; + let mut prev_margin_text = None; + if self.three_way { + if let Some((symbol, symbol_diff)) = get_symbol(state.prev_obj.as_ref(), self.prev_sym) + { + let mut text = Text::default(); + let rect = content_chunks[4].inner(Margin::new(0, 1)); + self.print_sym( + &mut text, + symbol, + symbol_diff, + rect, + &self.right_highlight, + result, + true, + ); + max_width = max_width.max(text.width()); + prev_text = Some(text); + + // Render margin + let mut text = Text::default(); + let rect = content_chunks[3].inner(Margin::new(1, 1)); + self.print_margin(&mut text, symbol_diff, rect); + prev_margin_text = Some(text); + } + } + + let max_scroll_x = + max_width.saturating_sub(content_chunks[0].width.min(content_chunks[2].width) as usize); + if self.scroll_x > max_scroll_x { + self.scroll_x = max_scroll_x; + } + self.scroll_state_x = + self.scroll_state_x.content_length(max_scroll_x).position(self.scroll_x); + + if let Some(text) = left_text { + // Render left column + f.render_widget( + Paragraph::new(text) + .block( + Block::new() + .borders(Borders::TOP) + .border_style(Style::new().fg(Color::Gray)) + .title_style(Style::new().bold()) + .title("TARGET"), + ) + .scroll((0, self.scroll_x as u16)), + content_chunks[0], + ); + } + if let Some(text) = margin_text { + f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1))); + } + if let Some(text) = right_text { + f.render_widget( + Paragraph::new(text) + .block( + Block::new() + .borders(Borders::TOP) + .border_style(Style::new().fg(Color::Gray)) + .title_style(Style::new().bold()) + .title("CURRENT"), + ) + .scroll((0, self.scroll_x as u16)), + content_chunks[2], + ); + } + + if self.three_way { + if let Some(text) = prev_margin_text { + f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1))); + } + let block = Block::new() + .borders(Borders::TOP) + .border_style(Style::new().fg(Color::Gray)) + .title_style(Style::new().bold()) + .title("SAVED"); + if let Some(text) = prev_text { + f.render_widget( + Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)), + content_chunks[4], + ); + } else { + f.render_widget(block, content_chunks[4]); + } + } + + // Render scrollbars + f.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None), + chunks[1].inner(Margin::new(0, 1)), + &mut self.scroll_state_y, + ); + f.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"), + content_chunks[0], + &mut self.scroll_state_x, + ); + f.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"), + content_chunks[2], + &mut self.scroll_state_x, + ); + if self.three_way { + f.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol("■"), + content_chunks[4], + &mut self.scroll_state_x, + ); + } + + if let Some(new_highlight) = left_highlight { + if new_highlight == self.left_highlight { + if self.left_highlight != self.right_highlight { + self.right_highlight = self.left_highlight.clone(); + } else { + self.left_highlight = HighlightKind::None; + self.right_highlight = HighlightKind::None; + } + } else { + self.left_highlight = new_highlight; + } + result.redraw = true; + } else if let Some(new_highlight) = right_highlight { + if new_highlight == self.right_highlight { + if self.left_highlight != self.right_highlight { + self.left_highlight = self.right_highlight.clone(); + } else { + self.left_highlight = HighlightKind::None; + self.right_highlight = HighlightKind::None; + } + } else { + self.right_highlight = new_highlight; + } + result.redraw = true; + } + + if self.open_options { + self.draw_options(f, result); + } + } + + fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow { + let mut result = EventResult::default(); + match event { + Event::Key(event) + if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) => + { + match event.code { + // Quit + KeyCode::Esc | KeyCode::Char('q') => return EventControlFlow::Break, + // Page up + KeyCode::PageUp => { + self.page_up(false); + result.redraw = true; + } + // Page up (shift + space) + KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => { + self.page_up(false); + result.redraw = true; + } + // Page down + KeyCode::Char(' ') | KeyCode::PageDown => { + self.page_down(false); + result.redraw = true; + } + // Page down (ctrl + f) + KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => { + self.page_down(false); + result.redraw = true; + } + // Page up (ctrl + b) + KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => { + self.page_up(false); + result.redraw = true; + } + // Half page down (ctrl + d) + KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => { + self.page_down(true); + result.redraw = true; + } + // Half page up (ctrl + u) + KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => { + self.page_up(true); + result.redraw = true; + } + // Scroll down + KeyCode::Down | KeyCode::Char('j') => { + self.scroll_y += 1; + result.redraw = true; + } + // Scroll up + KeyCode::Up | KeyCode::Char('k') => { + self.scroll_y = self.scroll_y.saturating_sub(1); + result.redraw = true; + } + // Scroll to start + KeyCode::Char('g') => { + self.scroll_y = 0; + result.redraw = true; + } + // Scroll to end + KeyCode::Char('G') => { + self.scroll_y = self.num_rows; + result.redraw = true; + } + // Reload + KeyCode::Char('r') => { + result.redraw = true; + return EventControlFlow::Reload; + } + // Scroll right + KeyCode::Right | KeyCode::Char('l') => { + self.scroll_x += 1; + result.redraw = true; + } + // Scroll left + KeyCode::Left | KeyCode::Char('h') => { + self.scroll_x = self.scroll_x.saturating_sub(1); + result.redraw = true; + } + // Toggle relax relocation diffs + KeyCode::Char('x') => { + state.relax_reloc_diffs = !state.relax_reloc_diffs; + result.redraw = true; + return EventControlFlow::Reload; + } + // Toggle three-way diff + KeyCode::Char('3') => { + self.three_way = !self.three_way; + result.redraw = true; + } + // Toggle options + KeyCode::Char('o') => { + self.open_options = !self.open_options; + result.redraw = true; + } + _ => {} + } + } + Event::Mouse(event) => match event.kind { + MouseEventKind::ScrollDown => { + self.scroll_y += 3; + result.redraw = true; + } + MouseEventKind::ScrollUp => { + self.scroll_y = self.scroll_y.saturating_sub(3); + result.redraw = true; + } + MouseEventKind::ScrollRight => { + self.scroll_x += 3; + result.redraw = true; + } + MouseEventKind::ScrollLeft => { + self.scroll_x = self.scroll_x.saturating_sub(3); + result.redraw = true; + } + MouseEventKind::Down(MouseButton::Left) => { + result.click_xy = Some((event.column, event.row)); + result.redraw = true; + } + _ => {} + }, + Event::Resize(_, _) => { + result.redraw = true; + } + _ => {} + } + EventControlFlow::Continue(result) + } + + fn reload(&mut self, state: &AppState) -> Result<()> { + let left_sym = + state.left_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name)); + let right_sym = + state.right_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name)); + let prev_sym = + state.prev_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name)); + self.num_rows = match ( + get_symbol(state.left_obj.as_ref(), left_sym), + get_symbol(state.right_obj.as_ref(), right_sym), + ) { + (Some((_l, ld)), Some((_r, rd))) => ld.instructions.len().max(rd.instructions.len()), + (Some((_l, ld)), None) => ld.instructions.len(), + (None, Some((_r, rd))) => rd.instructions.len(), + (None, None) => bail!("Symbol not found: {}", self.symbol_name), + }; + self.left_sym = left_sym; + self.right_sym = right_sym; + self.prev_sym = prev_sym; + Ok(()) + } +} + +impl FunctionDiffUi { + pub fn draw_options(&mut self, f: &mut Frame, _result: &mut EventResult) { + let percent_x = 50; + let percent_y = 50; + let popup_rect = Layout::vertical([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(f.area())[1]; + let popup_rect = Layout::horizontal([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_rect)[1]; + + let popup = Block::default() + .borders(Borders::ALL) + .title("Options") + .title_style(Style::default().fg(Color::White).bg(Color::Black)); + f.render_widget(Clear, popup_rect); + f.render_widget(popup, popup_rect); + } + + fn page_up(&mut self, half: bool) { + self.scroll_y = self.scroll_y.saturating_sub(self.per_page / if half { 2 } else { 1 }); + } + + fn page_down(&mut self, half: bool) { + self.scroll_y += self.per_page / if half { 2 } else { 1 }; + } + + #[allow(clippy::too_many_arguments)] + fn print_sym( + &self, + out: &mut Text<'static>, + symbol: &ObjSymbol, + symbol_diff: &ObjSymbolDiff, + rect: Rect, + highlight: &HighlightKind, + result: &EventResult, + only_changed: bool, + ) -> Option { + let base_addr = symbol.address; + let mut new_highlight = None; + for (y, ins_diff) in symbol_diff + .instructions + .iter() + .skip(self.scroll_y) + .take(rect.height as usize) + .enumerate() + { + if only_changed && ins_diff.kind == ObjInsDiffKind::None { + out.lines.push(Line::default()); + continue; + } + let mut sx = rect.x; + let sy = rect.y + y as u16; + let mut line = Line::default(); + display_diff(ins_diff, base_addr, |text| -> Result<()> { + let label_text; + let mut base_color = match ins_diff.kind { + ObjInsDiffKind::None + | ObjInsDiffKind::OpMismatch + | ObjInsDiffKind::ArgMismatch => Color::Gray, + ObjInsDiffKind::Replace => Color::Cyan, + ObjInsDiffKind::Delete => Color::Red, + ObjInsDiffKind::Insert => Color::Green, + }; + let mut pad_to = 0; + match text { + DiffText::Basic(text) => { + label_text = text.to_string(); + } + DiffText::BasicColor(s, idx) => { + label_text = s.to_string(); + base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()]; + } + DiffText::Line(num) => { + label_text = format!("{num} "); + base_color = Color::DarkGray; + pad_to = 5; + } + DiffText::Address(addr) => { + label_text = format!("{:x}:", addr); + pad_to = 5; + } + DiffText::Opcode(mnemonic, _op) => { + label_text = mnemonic.to_string(); + if ins_diff.kind == ObjInsDiffKind::OpMismatch { + base_color = Color::Blue; + } + pad_to = 8; + } + DiffText::Argument(arg, diff) => { + label_text = arg.to_string(); + if let Some(diff) = diff { + base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()] + } + } + DiffText::BranchDest(addr, diff) => { + label_text = format!("{addr:x}"); + if let Some(diff) = diff { + base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()] + } + } + DiffText::Symbol(sym) => { + let name = sym.demangled_name.as_ref().unwrap_or(&sym.name); + label_text = name.clone(); + base_color = Color::White; + } + DiffText::Spacing(n) => { + line.spans.push(Span::raw(" ".repeat(n))); + sx += n as u16; + return Ok(()); + } + DiffText::Eol => { + return Ok(()); + } + } + let len = label_text.len(); + let highlighted = *highlight == text; + if let Some((cx, cy)) = result.click_xy { + if cx >= sx && cx < sx + len as u16 && cy == sy { + new_highlight = Some(text.into()); + } + } + let mut style = Style::new().fg(base_color); + if highlighted { + style = style.bg(Color::DarkGray); + } + line.spans.push(Span::styled(label_text, style)); + sx += len as u16; + if pad_to > len { + let pad = (pad_to - len) as u16; + line.spans.push(Span::raw(" ".repeat(pad as usize))); + sx += pad; + } + Ok(()) + }) + .unwrap(); + out.lines.push(line); + } + new_highlight + } + + fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) { + for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) { + if ins_diff.kind != ObjInsDiffKind::None { + out.lines.push(Line::raw(match ins_diff.kind { + ObjInsDiffKind::Delete => "<", + ObjInsDiffKind::Insert => ">", + _ => "|", + })); + } else { + out.lines.push(Line::raw(" ")); + } + } + } +} + +pub const COLOR_ROTATION: [Color; 7] = [ + Color::Magenta, + Color::Cyan, + Color::Green, + Color::Red, + Color::Yellow, + Color::Blue, + Color::Green, +]; + +pub fn match_percent_color(match_percent: f32) -> Color { + if match_percent == 100.0 { + Color::Green + } else if match_percent >= 50.0 { + Color::LightBlue + } else { + Color::LightRed + } +} + +#[inline] +fn get_symbol( + obj: Option<&(ObjInfo, ObjDiff)>, + sym: Option, +) -> Option<(&ObjSymbol, &ObjSymbolDiff)> { + let (obj, diff) = obj?; + let sym = sym?; + Some((obj.section_symbol(sym).1, diff.symbol_diff(sym))) +} + +fn find_function(obj: &ObjInfo, name: &str) -> Option { + for (section_idx, section) in obj.sections.iter().enumerate() { + if section.kind != ObjSectionKind::Code { + continue; + } + for (symbol_idx, symbol) in section.symbols.iter().enumerate() { + if symbol.name == name { + return Some(SymbolRef { section_idx, symbol_idx }); + } + } + } + None +} diff --git a/objdiff-cli/src/views/mod.rs b/objdiff-cli/src/views/mod.rs new file mode 100644 index 0000000..88e6354 --- /dev/null +++ b/objdiff-cli/src/views/mod.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use crossterm::event::Event; +use ratatui::Frame; + +use crate::cmd::diff::AppState; + +pub mod function_diff; + +#[derive(Default)] +pub struct EventResult { + pub redraw: bool, + pub click_xy: Option<(u16, u16)>, +} + +pub enum EventControlFlow { + Break, + Continue(EventResult), + Reload, +} + +pub trait UiView { + fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult); + fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow; + fn reload(&mut self, state: &AppState) -> Result<()>; +} diff --git a/objdiff-core/Cargo.toml b/objdiff-core/Cargo.toml index 7b4deb9..969601e 100644 --- a/objdiff-core/Cargo.toml +++ b/objdiff-core/Cargo.toml @@ -16,15 +16,16 @@ documentation = "https://docs.rs/objdiff-core" crate-type = ["cdylib", "rlib"] [features] -all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"] +all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings", "build"] any-arch = ["bimap"] # Implicit, used to check if any arch is enabled +bindings = ["serde_json", "prost", "pbjson"] +build = ["shell-escape", "path-slash", "winapi", "notify", "notify-debouncer-full", "reqwest", "self_update", "tempfile", "time"] config = ["bimap", "globset", "semver", "serde_json", "serde_yaml"] dwarf = ["gimli"] mips = ["any-arch", "rabbitizer"] ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"] x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"] arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"] -bindings = ["serde_json", "prost", "pbjson"] wasm = ["bindings", "console_error_panic_hook", "console_log"] [package.metadata.docs.rs] @@ -76,6 +77,27 @@ msvc-demangler = { version = "0.10", optional = true } unarm = { version = "1.6", optional = true } arm-attr = { version = "0.1", optional = true } +# build +notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true } +notify-debouncer-full = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true } +shell-escape = { version = "0.1", optional = true } +tempfile = { version = "3.13", optional = true } +time = { version = "0.3", optional = true } + +[target.'cfg(windows)'.dependencies] +path-slash = { version = "0.2", optional = true } +winapi = { version = "0.3", optional = true } + +# For Linux static binaries, use rustls +[target.'cfg(target_os = "linux")'.dependencies] +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true } +self_update = { version = "0.41", default-features = false, features = ["rustls"], optional = true } + +# For all other platforms, use native TLS +[target.'cfg(not(target_os = "linux"))'.dependencies] +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true } +self_update = { version = "0.41", optional = true } + [build-dependencies] prost-build = "0.13" pbjson-build = "0.7" diff --git a/objdiff-core/src/build/mod.rs b/objdiff-core/src/build/mod.rs new file mode 100644 index 0000000..d8bae78 --- /dev/null +++ b/objdiff-core/src/build/mod.rs @@ -0,0 +1,106 @@ +pub mod watcher; + +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +pub struct BuildStatus { + pub success: bool, + pub cmdline: String, + pub stdout: String, + pub stderr: String, +} + +impl Default for BuildStatus { + fn default() -> Self { + BuildStatus { + success: true, + cmdline: String::new(), + stdout: String::new(), + stderr: String::new(), + } + } +} + +#[derive(Debug, Clone)] +pub struct BuildConfig { + pub project_dir: Option, + pub custom_make: Option, + pub custom_args: Option>, + #[allow(unused)] + pub selected_wsl_distro: Option, +} + +pub fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus { + let Some(cwd) = &config.project_dir else { + return BuildStatus { + success: false, + stderr: "Missing project dir".to_string(), + ..Default::default() + }; + }; + let make = config.custom_make.as_deref().unwrap_or("make"); + let make_args = config.custom_args.as_deref().unwrap_or(&[]); + #[cfg(not(windows))] + let mut command = { + let mut command = Command::new(make); + command.current_dir(cwd).args(make_args).arg(arg); + command + }; + #[cfg(windows)] + let mut command = { + use std::os::windows::process::CommandExt; + + use path_slash::PathExt; + let mut command = if config.selected_wsl_distro.is_some() { + Command::new("wsl") + } else { + Command::new(make) + }; + if let Some(distro) = &config.selected_wsl_distro { + // Strip distro root prefix \\wsl.localhost\{distro} + let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro); + let cwd = match cwd.strip_prefix(wsl_path_prefix) { + Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()), + Err(_) => cwd.to_string_lossy().to_string(), + }; + + command + .arg("--cd") + .arg(cwd) + .arg("-d") + .arg(distro) + .arg("--") + .arg(make) + .args(make_args) + .arg(arg.to_slash_lossy().as_ref()); + } else { + command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref()); + } + command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW); + command + }; + let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned(); + for arg in command.get_args() { + cmdline.push(' '); + cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref()); + } + let output = match command.output() { + Ok(output) => output, + Err(e) => { + return BuildStatus { + success: false, + cmdline, + stdout: Default::default(), + stderr: e.to_string(), + }; + } + }; + // Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy + let stdout = String::from_utf8(output.stdout) + .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()); + let stderr = String::from_utf8(output.stderr) + .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()); + BuildStatus { success: output.status.success(), cmdline, stdout, stderr } +} diff --git a/objdiff-core/src/build/watcher.rs b/objdiff-core/src/build/watcher.rs new file mode 100644 index 0000000..ef0cc60 --- /dev/null +++ b/objdiff-core/src/build/watcher.rs @@ -0,0 +1,72 @@ +use std::{ + fs, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + task::Waker, + time::Duration, +}; + +use globset::GlobSet; +use notify::RecursiveMode; +use notify_debouncer_full::{new_debouncer_opt, DebounceEventResult}; + +pub type Watcher = notify_debouncer_full::Debouncer< + notify::RecommendedWatcher, + notify_debouncer_full::RecommendedCache, +>; + +pub struct WatcherState { + pub config_path: Option, + pub left_obj_path: Option, + pub right_obj_path: Option, + pub patterns: GlobSet, +} + +pub fn create_watcher( + modified: Arc, + project_dir: &Path, + patterns: GlobSet, + waker: Waker, +) -> notify::Result { + let base_dir = fs::canonicalize(project_dir)?; + let base_dir_clone = base_dir.clone(); + let timeout = Duration::from_millis(200); + let config = notify::Config::default().with_poll_interval(Duration::from_secs(2)); + let mut debouncer = new_debouncer_opt( + timeout, + None, + move |result: DebounceEventResult| match result { + Ok(events) => { + let patterns = &patterns; + for event in events.iter() { + if !matches!( + event.kind, + notify::EventKind::Modify(..) + | notify::EventKind::Create(..) + | notify::EventKind::Remove(..) + ) { + continue; + } + for path in &event.paths { + let Ok(path) = path.strip_prefix(&base_dir_clone) else { + continue; + }; + if patterns.is_match(path) { + // log::info!("File modified: {}", path.display()); + modified.store(true, Ordering::Relaxed); + waker.wake_by_ref(); + } + } + } + } + Err(errors) => errors.iter().for_each(|e| log::error!("Watch error: {e:?}")), + }, + notify_debouncer_full::RecommendedCache::new(), + config, + )?; + debouncer.watch(base_dir, RecursiveMode::Recursive)?; + Ok(debouncer) +} diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs index 09d064b..28ddd82 100644 --- a/objdiff-core/src/config/mod.rs +++ b/objdiff-core/src/config/mod.rs @@ -174,6 +174,10 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[ "*.inc", "*.py", "*.yml", "*.txt", "*.json", ]; +pub fn default_watch_patterns() -> Vec { + DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() +} + #[derive(Clone, Eq, PartialEq)] pub struct ProjectConfigInfo { pub path: PathBuf, diff --git a/objdiff-core/src/jobs/check_update.rs b/objdiff-core/src/jobs/check_update.rs new file mode 100644 index 0000000..3d33bf3 --- /dev/null +++ b/objdiff-core/src/jobs/check_update.rs @@ -0,0 +1,50 @@ +use std::{sync::mpsc::Receiver, task::Waker}; + +use anyhow::{Context, Result}; +use self_update::{ + cargo_crate_version, + update::{Release, ReleaseUpdate}, +}; + +use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}; + +pub struct CheckUpdateConfig { + pub build_updater: fn() -> Result>, + pub bin_names: Vec, +} + +pub struct CheckUpdateResult { + pub update_available: bool, + pub latest_release: Release, + pub found_binary: Option, +} + +fn run_check_update( + context: &JobContext, + cancel: Receiver<()>, + config: CheckUpdateConfig, +) -> Result> { + update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?; + let updater = (config.build_updater)().context("Failed to create release updater")?; + let latest_release = updater.get_latest_release()?; + let update_available = + self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?; + // Find the binary name in the release assets + let mut found_binary = None; + for bin_name in &config.bin_names { + if latest_release.assets.iter().any(|a| &a.name == bin_name) { + found_binary = Some(bin_name.clone()); + break; + } + } + + update_status(context, "Complete".to_string(), 1, 1, &cancel)?; + Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary })) +} + +pub fn start_check_update(waker: Waker, config: CheckUpdateConfig) -> JobState { + start_job(waker, "Check for updates", Job::CheckUpdate, move |context, cancel| { + run_check_update(&context, cancel, config) + .map(|result| JobResult::CheckUpdate(Some(result))) + }) +} diff --git a/objdiff-gui/src/jobs/create_scratch.rs b/objdiff-core/src/jobs/create_scratch.rs similarity index 64% rename from objdiff-gui/src/jobs/create_scratch.rs rename to objdiff-core/src/jobs/create_scratch.rs index 3d8ecd4..76fa3d0 100644 --- a/objdiff-gui/src/jobs/create_scratch.rs +++ b/objdiff-core/src/jobs/create_scratch.rs @@ -1,14 +1,10 @@ -use std::{fs, path::PathBuf, sync::mpsc::Receiver}; +use std::{fs, path::PathBuf, sync::mpsc::Receiver, task::Waker}; use anyhow::{anyhow, bail, Context, Result}; -use const_format::formatcp; use crate::{ - app::AppConfig, - jobs::{ - objdiff::{run_make, BuildConfig, BuildStatus}, - start_job, update_status, Job, JobContext, JobResult, JobState, - }, + build::{run_make, BuildConfig, BuildStatus}, + jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, }; #[derive(Debug, Clone)] @@ -25,37 +21,6 @@ pub struct CreateScratchConfig { pub target_obj: PathBuf, } -impl CreateScratchConfig { - pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result { - let Some(selected_obj) = &config.selected_obj else { - bail!("No object selected"); - }; - let Some(target_path) = &selected_obj.target_path else { - bail!("No target path for {}", selected_obj.name); - }; - let Some(scratch_config) = &selected_obj.scratch else { - bail!("No scratch configuration for {}", selected_obj.name); - }; - Ok(Self { - build_config: BuildConfig::from_config(config), - context_path: scratch_config.ctx_path.clone(), - build_context: scratch_config.build_ctx.unwrap_or(false), - compiler: scratch_config.compiler.clone().unwrap_or_default(), - platform: scratch_config.platform.clone().unwrap_or_default(), - compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), - function_name, - target_obj: target_path.to_path_buf(), - }) - } - - pub fn is_available(config: &AppConfig) -> bool { - let Some(selected_obj) = &config.selected_obj else { - return false; - }; - selected_obj.target_path.is_some() && selected_obj.scratch.is_some() - } -} - #[derive(Default, Debug, Clone)] pub struct CreateScratchResult { pub scratch_url: String, @@ -97,7 +62,7 @@ fn run_create_scratch( update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?; let diff_flags = [format!("--disassemble={}", config.function_name)]; - let diff_flags = serde_json::to_string(&diff_flags).unwrap(); + let diff_flags = serde_json::to_string(&diff_flags)?; let obj_path = project_dir.join(&config.target_obj); let file = reqwest::blocking::multipart::Part::file(&obj_path) .with_context(|| format!("Failed to open {}", obj_path.display()))?; @@ -112,7 +77,7 @@ fn run_create_scratch( .part("target_obj", file); let client = reqwest::blocking::Client::new(); let response = client - .post(formatcp!("{API_HOST}/api/scratch")) + .post(format!("{API_HOST}/api/scratch")) .multipart(form) .send() .map_err(|e| anyhow!("Failed to send request: {}", e))?; @@ -126,8 +91,8 @@ fn run_create_scratch( Ok(Box::from(CreateScratchResult { scratch_url })) } -pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState { - start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| { +pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState { + start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| { run_create_scratch(&context, cancel, config) .map(|result| JobResult::CreateScratch(Some(result))) }) diff --git a/objdiff-gui/src/jobs/mod.rs b/objdiff-core/src/jobs/mod.rs similarity index 71% rename from objdiff-gui/src/jobs/mod.rs rename to objdiff-core/src/jobs/mod.rs index 1cf728f..7e070e6 100644 --- a/objdiff-gui/src/jobs/mod.rs +++ b/objdiff-core/src/jobs/mod.rs @@ -4,6 +4,7 @@ use std::{ mpsc::{Receiver, Sender, TryRecvError}, Arc, RwLock, }, + task::Waker, thread::JoinHandle, }; @@ -53,7 +54,6 @@ impl JobQueue { } /// Returns whether any job is running. - #[expect(dead_code)] pub fn any_running(&self) -> bool { self.jobs.iter().any(|job| { if let Some(handle) = &job.handle { @@ -96,12 +96,53 @@ impl JobQueue { /// Removes a job from the queue given its ID. pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); } + + /// Collects the results of all finished jobs and handles any errors. + pub fn collect_results(&mut self) { + let mut results = vec![]; + for (job, result) in self.iter_finished() { + match result { + Ok(result) => { + match result { + JobResult::None => { + // Job context contains the error + } + _ => results.push(result), + } + } + Err(err) => { + let err = if let Some(msg) = err.downcast_ref::<&'static str>() { + anyhow::Error::msg(*msg) + } else if let Some(msg) = err.downcast_ref::() { + anyhow::Error::msg(msg.clone()) + } else { + anyhow::Error::msg("Thread panicked") + }; + let result = job.context.status.write(); + if let Ok(mut guard) = result { + guard.error = Some(err); + } else { + drop(result); + job.context.status = Arc::new(RwLock::new(JobStatus { + title: "Error".to_string(), + progress_percent: 0.0, + progress_items: None, + status: String::new(), + error: Some(err), + })); + } + } + } + } + self.results.append(&mut results); + self.clear_finished(); + } } #[derive(Clone)] pub struct JobContext { pub status: Arc>, - pub egui: egui::Context, + pub waker: Waker, } pub struct JobState { @@ -137,7 +178,7 @@ fn should_cancel(rx: &Receiver<()>) -> bool { } fn start_job( - ctx: &egui::Context, + waker: Waker, title: &str, kind: Job, run: impl FnOnce(JobContext, Receiver<()>) -> Result + Send + 'static, @@ -149,8 +190,8 @@ fn start_job( status: String::new(), error: None, })); - let context = JobContext { status: status.clone(), egui: ctx.clone() }; - let context_inner = JobContext { status: status.clone(), egui: ctx.clone() }; + let context = JobContext { status: status.clone(), waker: waker.clone() }; + let context_inner = JobContext { status: status.clone(), waker }; let (tx, rx) = std::sync::mpsc::channel(); let handle = std::thread::spawn(move || { return match run(context_inner, rx) { @@ -164,7 +205,7 @@ fn start_job( }; }); let id = JOB_ID.fetch_add(1, Ordering::Relaxed); - log::info!("Started job {}", id); + // log::info!("Started job {}", id); TODO JobState { id, kind, handle: Some(handle), context, cancel: tx } } @@ -186,6 +227,6 @@ fn update_status( w.status = str; } drop(w); - context.egui.request_repaint(); + context.waker.wake_by_ref(); Ok(()) } diff --git a/objdiff-core/src/jobs/objdiff.rs b/objdiff-core/src/jobs/objdiff.rs new file mode 100644 index 0000000..d9c33af --- /dev/null +++ b/objdiff-core/src/jobs/objdiff.rs @@ -0,0 +1,199 @@ +use std::{path::PathBuf, sync::mpsc::Receiver, task::Waker}; + +use anyhow::{anyhow, Error, Result}; +use time::OffsetDateTime; + +use crate::{ + build::{run_make, BuildConfig, BuildStatus}, + config::SymbolMappings, + diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff}, + jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, + obj::{read, ObjInfo}, +}; + +pub struct ObjDiffConfig { + pub build_config: BuildConfig, + pub build_base: bool, + pub build_target: bool, + pub target_path: Option, + pub base_path: Option, + pub diff_obj_config: DiffObjConfig, + pub symbol_mappings: SymbolMappings, + pub selecting_left: Option, + pub selecting_right: Option, +} + +pub struct ObjDiffResult { + pub first_status: BuildStatus, + pub second_status: BuildStatus, + pub first_obj: Option<(ObjInfo, ObjDiff)>, + pub second_obj: Option<(ObjInfo, ObjDiff)>, + pub time: OffsetDateTime, +} + +fn run_build( + context: &JobContext, + cancel: Receiver<()>, + mut config: ObjDiffConfig, +) -> Result> { + // Use the per-object symbol mappings, we don't set mappings globally + config.diff_obj_config.symbol_mappings = MappingConfig { + mappings: config.symbol_mappings, + selecting_left: config.selecting_left, + selecting_right: config.selecting_right, + }; + + let mut target_path_rel = None; + let mut base_path_rel = None; + if config.build_target || config.build_base { + let project_dir = config + .build_config + .project_dir + .as_ref() + .ok_or_else(|| Error::msg("Missing project dir"))?; + if let Some(target_path) = &config.target_path { + target_path_rel = Some(target_path.strip_prefix(project_dir).map_err(|_| { + anyhow!( + "Target path '{}' doesn't begin with '{}'", + target_path.display(), + project_dir.display() + ) + })?); + } + if let Some(base_path) = &config.base_path { + base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| { + anyhow!( + "Base path '{}' doesn't begin with '{}'", + base_path.display(), + project_dir.display() + ) + })?); + }; + } + + let mut total = 1; + if config.build_target && target_path_rel.is_some() { + total += 1; + } + if config.build_base && base_path_rel.is_some() { + total += 1; + } + if config.target_path.is_some() { + total += 1; + } + if config.base_path.is_some() { + total += 1; + } + + let mut step_idx = 0; + let mut first_status = match target_path_rel { + Some(target_path_rel) if config.build_target => { + update_status( + context, + format!("Building target {}", target_path_rel.display()), + step_idx, + total, + &cancel, + )?; + step_idx += 1; + run_make(&config.build_config, target_path_rel) + } + _ => BuildStatus::default(), + }; + + let mut second_status = match base_path_rel { + Some(base_path_rel) if config.build_base => { + update_status( + context, + format!("Building base {}", base_path_rel.display()), + step_idx, + total, + &cancel, + )?; + step_idx += 1; + run_make(&config.build_config, base_path_rel) + } + _ => BuildStatus::default(), + }; + + let time = OffsetDateTime::now_utc(); + + let first_obj = match &config.target_path { + Some(target_path) if first_status.success => { + update_status( + context, + format!("Loading target {}", target_path.display()), + step_idx, + total, + &cancel, + )?; + step_idx += 1; + match read::read(target_path, &config.diff_obj_config) { + Ok(obj) => Some(obj), + Err(e) => { + first_status = BuildStatus { + success: false, + stdout: format!("Loading object '{}'", target_path.display()), + stderr: format!("{:#}", e), + ..Default::default() + }; + None + } + } + } + Some(_) => { + step_idx += 1; + None + } + _ => None, + }; + + let second_obj = match &config.base_path { + Some(base_path) if second_status.success => { + update_status( + context, + format!("Loading base {}", base_path.display()), + step_idx, + total, + &cancel, + )?; + step_idx += 1; + match read::read(base_path, &config.diff_obj_config) { + Ok(obj) => Some(obj), + Err(e) => { + second_status = BuildStatus { + success: false, + stdout: format!("Loading object '{}'", base_path.display()), + stderr: format!("{:#}", e), + ..Default::default() + }; + None + } + } + } + Some(_) => { + step_idx += 1; + None + } + _ => None, + }; + + update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?; + step_idx += 1; + let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?; + + update_status(context, "Complete".to_string(), step_idx, total, &cancel)?; + Ok(Box::new(ObjDiffResult { + first_status, + second_status, + first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))), + second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))), + time, + })) +} + +pub fn start_build(waker: Waker, config: ObjDiffConfig) -> JobState { + start_job(waker, "Build", Job::ObjDiff, move |context, cancel| { + run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result))) + }) +} diff --git a/objdiff-gui/src/jobs/update.rs b/objdiff-core/src/jobs/update.rs similarity index 59% rename from objdiff-gui/src/jobs/update.rs rename to objdiff-core/src/jobs/update.rs index 178dd7a..a6427b3 100644 --- a/objdiff-gui/src/jobs/update.rs +++ b/objdiff-core/src/jobs/update.rs @@ -1,16 +1,21 @@ use std::{ env::{current_dir, current_exe}, - fs::File, + fs::{File, Permissions}, path::PathBuf, sync::mpsc::Receiver, + task::Waker, }; use anyhow::{Context, Result}; +pub use self_update; // Re-export self_update crate +use self_update::update::ReleaseUpdate; -use crate::{ - jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, - update::build_updater, -}; +use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}; + +pub struct UpdateConfig { + pub build_updater: fn() -> Result>, + pub bin_name: String, +} pub struct UpdateResult { pub exe_path: PathBuf, @@ -19,16 +24,15 @@ pub struct UpdateResult { fn run_update( status: &JobContext, cancel: Receiver<()>, - bin_name: String, + config: UpdateConfig, ) -> Result> { update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?; - let updater = build_updater().context("Failed to create release updater")?; + let updater = (config.build_updater)().context("Failed to create release updater")?; let latest_release = updater.get_latest_release()?; - let asset = latest_release - .assets - .iter() - .find(|a| a.name == bin_name) - .ok_or_else(|| anyhow::Error::msg(format!("No release asset for {bin_name}")))?; + let asset = + latest_release.assets.iter().find(|a| a.name == config.bin_name).ok_or_else(|| { + anyhow::Error::msg(format!("No release asset for {}", config.bin_name)) + })?; update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?; let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?; @@ -47,9 +51,7 @@ fn run_update( #[cfg(unix)] { use std::{fs, os::unix::fs::PermissionsExt}; - let mut perms = fs::metadata(&target_file)?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&target_file, perms)?; + fs::set_permissions(&target_file, Permissions::from_mode(0o755))?; } tmp_dir.close()?; @@ -57,8 +59,8 @@ fn run_update( Ok(Box::from(UpdateResult { exe_path: target_file })) } -pub fn start_update(ctx: &egui::Context, bin_name: String) -> JobState { - start_job(ctx, "Update app", Job::Update, move |context, cancel| { - run_update(&context, cancel, bin_name).map(JobResult::Update) +pub fn start_update(waker: Waker, config: UpdateConfig) -> JobState { + start_job(waker, "Update app", Job::Update, move |context, cancel| { + run_update(&context, cancel, config).map(JobResult::Update) }) } diff --git a/objdiff-core/src/lib.rs b/objdiff-core/src/lib.rs index 02df5a4..33e61fe 100644 --- a/objdiff-core/src/lib.rs +++ b/objdiff-core/src/lib.rs @@ -2,10 +2,14 @@ pub mod arch; #[cfg(feature = "bindings")] pub mod bindings; +#[cfg(feature = "build")] +pub mod build; #[cfg(feature = "config")] pub mod config; #[cfg(feature = "any-arch")] pub mod diff; +#[cfg(feature = "build")] +pub mod jobs; #[cfg(feature = "any-arch")] pub mod obj; pub mod util; diff --git a/objdiff-gui/Cargo.toml b/objdiff-gui/Cargo.toml index 8be0c75..136901f 100644 --- a/objdiff-gui/Cargo.toml +++ b/objdiff-gui/Cargo.toml @@ -38,7 +38,6 @@ float-ord = "0.3" font-kit = "0.14" globset = { version = "0.4", features = ["serde1"] } log = "0.4" -notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" } objdiff-core = { path = "../objdiff-core", features = ["all"] } open = "5.3" png = "0.17" @@ -51,7 +50,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" shell-escape = "0.1" strum = { version = "0.26", features = ["derive"] } -tempfile = "3.13" time = { version = "0.3", features = ["formatting", "local-offset"] } # Keep version in sync with egui @@ -76,20 +74,6 @@ features = [ optional = true default-features = false -# For Linux static binaries, use rustls -[target.'cfg(target_os = "linux")'.dependencies] -reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] } -self_update = { version = "0.41", default-features = false, features = ["rustls"] } - -# For all other platforms, use native TLS -[target.'cfg(not(target_os = "linux"))'.dependencies] -reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] } -self_update = "0.41" - -[target.'cfg(windows)'.dependencies] -path-slash = "0.2" -winapi = "0.3" - [target.'cfg(unix)'.dependencies] exec = "0.3" diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index b79d1c6..dbc56ac 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -11,24 +11,22 @@ use std::{ }; use filetime::FileTime; -use globset::{Glob, GlobSet}; -use notify::{RecursiveMode, Watcher}; +use globset::Glob; use objdiff_core::{ + build::watcher::{create_watcher, Watcher}, config::{ - build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject, - ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS, + build_globset, default_watch_patterns, save_project_config, ProjectConfig, + ProjectConfigInfo, ProjectObject, ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS, }, diff::DiffObjConfig, + jobs::{Job, JobQueue, JobResult}, }; use time::UtcOffset; use crate::{ app_config::{deserialize_config, AppConfigVersion}, config::{load_project_config, ProjectObjectNode}, - jobs::{ - objdiff::{start_build, ObjDiffConfig}, - Job, JobQueue, JobResult, JobStatus, - }, + jobs::{create_objdiff_config, egui_waker, start_build}, views::{ appearance::{appearance_window, Appearance}, config::{ @@ -121,11 +119,6 @@ impl From<&ProjectObject> for ObjectConfig { #[inline] fn bool_true() -> bool { true } -#[inline] -fn default_watch_patterns() -> Vec { - DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() -} - pub struct AppState { pub config: AppConfig, pub objects: Vec, @@ -399,7 +392,7 @@ pub struct App { view_state: ViewState, state: AppStateRef, modified: Arc, - watcher: Option, + watcher: Option, app_path: Option, relaunch_path: Rc>>, should_relaunch: bool, @@ -474,53 +467,17 @@ impl App { let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state; - let mut results = vec![]; - for (job, result) in jobs.iter_finished() { - match result { - Ok(result) => { - log::info!("Job {} finished", job.id); - match result { - JobResult::None => { - if let Some(err) = &job.context.status.read().unwrap().error { - log::error!("{:?}", err); - } - } - JobResult::Update(state) => { - if let Ok(mut guard) = self.relaunch_path.lock() { - *guard = Some(state.exe_path); - self.should_relaunch = true; - } - } - _ => results.push(result), - } - } - Err(err) => { - let err = if let Some(msg) = err.downcast_ref::<&'static str>() { - anyhow::Error::msg(*msg) - } else if let Some(msg) = err.downcast_ref::() { - anyhow::Error::msg(msg.clone()) - } else { - anyhow::Error::msg("Thread panicked") - }; - let result = job.context.status.write(); - if let Ok(mut guard) = result { - guard.error = Some(err); - } else { - drop(result); - job.context.status = Arc::new(RwLock::new(JobStatus { - title: "Error".to_string(), - progress_percent: 0.0, - progress_items: None, - status: String::new(), - error: Some(err), - })); - } + jobs.collect_results(); + jobs.results.retain(|result| match result { + JobResult::Update(state) => { + if let Ok(mut guard) = self.relaunch_path.lock() { + *guard = Some(state.exe_path.clone()); + self.should_relaunch = true; } + false } - } - jobs.results.append(&mut results); - jobs.clear_finished(); - + _ => true, + }); diff_state.pre_update(jobs, &self.state); config_state.pre_update(jobs, &self.state); debug_assert!(jobs.results.is_empty()); @@ -572,7 +529,7 @@ impl App { match build_globset(&state.config.watch_patterns) .map_err(anyhow::Error::new) .and_then(|globset| { - create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset) + create_watcher(self.modified.clone(), project_dir, globset, egui_waker(ctx)) .map_err(anyhow::Error::new) }) { Ok(watcher) => self.watcher = Some(watcher), @@ -619,15 +576,15 @@ impl App { && state.config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) { - jobs.push(start_build(ctx, ObjDiffConfig::from_state(state))); + start_build(ctx, jobs, create_objdiff_config(state)); state.queue_build = false; state.queue_reload = false; } else if state.queue_reload && !jobs.is_running(Job::ObjDiff) { - let mut diff_config = ObjDiffConfig::from_state(state); + let mut diff_config = create_objdiff_config(state); // Don't build, just reload the current files diff_config.build_base = false; diff_config.build_target = false; - jobs.push(start_build(ctx, diff_config)); + start_build(ctx, jobs, diff_config); state.queue_reload = false; } @@ -854,40 +811,6 @@ impl eframe::App for App { } } -fn create_watcher( - ctx: egui::Context, - modified: Arc, - project_dir: &Path, - patterns: GlobSet, -) -> notify::Result { - let base_dir = project_dir.to_owned(); - let mut watcher = - notify::recommended_watcher(move |res: notify::Result| match res { - Ok(event) => { - if matches!( - event.kind, - notify::EventKind::Modify(..) - | notify::EventKind::Create(..) - | notify::EventKind::Remove(..) - ) { - for path in &event.paths { - let Ok(path) = path.strip_prefix(&base_dir) else { - continue; - }; - if patterns.is_match(path) { - log::info!("File modified: {}", path.display()); - modified.store(true, Ordering::Relaxed); - ctx.request_repaint(); - } - } - } - } - Err(e) => log::error!("watch error: {e:?}"), - })?; - watcher.watch(project_dir, RecursiveMode::Recursive)?; - Ok(watcher) -} - #[inline] fn file_modified(path: &Path, last_ts: FileTime) -> bool { if let Ok(metadata) = fs::metadata(path) { diff --git a/objdiff-gui/src/jobs.rs b/objdiff-gui/src/jobs.rs new file mode 100644 index 0000000..34f22bb --- /dev/null +++ b/objdiff-gui/src/jobs.rs @@ -0,0 +1,137 @@ +use std::{ + sync::Arc, + task::{Wake, Waker}, +}; + +use anyhow::{bail, Result}; +use jobs::create_scratch; +use objdiff_core::{ + build::BuildConfig, + jobs, + jobs::{check_update::CheckUpdateConfig, objdiff, update::UpdateConfig, Job, JobQueue}, +}; + +use crate::{ + app::{AppConfig, AppState}, + update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD}, +}; + +struct EguiWaker(egui::Context); + +impl Wake for EguiWaker { + fn wake(self: Arc) { self.0.request_repaint(); } + + fn wake_by_ref(self: &Arc) { self.0.request_repaint(); } +} + +pub fn egui_waker(ctx: &egui::Context) -> Waker { Waker::from(Arc::new(EguiWaker(ctx.clone()))) } + +pub fn is_create_scratch_available(config: &AppConfig) -> bool { + let Some(selected_obj) = &config.selected_obj else { + return false; + }; + selected_obj.target_path.is_some() && selected_obj.scratch.is_some() +} + +pub fn start_create_scratch( + ctx: &egui::Context, + jobs: &mut JobQueue, + state: &AppState, + function_name: String, +) { + match create_scratch_config(state, function_name) { + Ok(config) => { + jobs.push_once(Job::CreateScratch, || { + create_scratch::start_create_scratch(egui_waker(ctx), config) + }); + } + Err(err) => { + log::error!("Failed to create scratch config: {err}"); + } + } +} + +fn create_scratch_config( + state: &AppState, + function_name: String, +) -> Result { + let Some(selected_obj) = &state.config.selected_obj else { + bail!("No object selected"); + }; + let Some(target_path) = &selected_obj.target_path else { + bail!("No target path for {}", selected_obj.name); + }; + let Some(scratch_config) = &selected_obj.scratch else { + bail!("No scratch configuration for {}", selected_obj.name); + }; + Ok(create_scratch::CreateScratchConfig { + build_config: BuildConfig::from(&state.config), + context_path: scratch_config.ctx_path.clone(), + build_context: scratch_config.build_ctx.unwrap_or(false), + compiler: scratch_config.compiler.clone().unwrap_or_default(), + platform: scratch_config.platform.clone().unwrap_or_default(), + compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), + function_name, + target_obj: target_path.to_path_buf(), + }) +} + +impl From<&AppConfig> for BuildConfig { + fn from(config: &AppConfig) -> Self { + Self { + project_dir: config.project_dir.clone(), + custom_make: config.custom_make.clone(), + custom_args: config.custom_args.clone(), + selected_wsl_distro: config.selected_wsl_distro.clone(), + } + } +} + +pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig { + objdiff::ObjDiffConfig { + build_config: BuildConfig::from(&state.config), + build_base: state.config.build_base, + build_target: state.config.build_target, + target_path: state + .config + .selected_obj + .as_ref() + .and_then(|obj| obj.target_path.as_ref()) + .cloned(), + base_path: state + .config + .selected_obj + .as_ref() + .and_then(|obj| obj.base_path.as_ref()) + .cloned(), + diff_obj_config: state.config.diff_obj_config.clone(), + symbol_mappings: state + .config + .selected_obj + .as_ref() + .map(|obj| &obj.symbol_mappings) + .cloned() + .unwrap_or_default(), + selecting_left: state.selecting_left.clone(), + selecting_right: state.selecting_right.clone(), + } +} + +pub fn start_build(ctx: &egui::Context, jobs: &mut JobQueue, config: objdiff::ObjDiffConfig) { + jobs.push_once(Job::ObjDiff, || objdiff::start_build(egui_waker(ctx), config)); +} + +pub fn start_check_update(ctx: &egui::Context, jobs: &mut JobQueue) { + jobs.push_once(Job::Update, || { + jobs::check_update::start_check_update(egui_waker(ctx), CheckUpdateConfig { + build_updater, + bin_names: vec![BIN_NAME_NEW.to_string(), BIN_NAME_OLD.to_string()], + }) + }); +} + +pub fn start_update(ctx: &egui::Context, jobs: &mut JobQueue, bin_name: String) { + jobs.push_once(Job::Update, || { + jobs::update::start_update(egui_waker(ctx), UpdateConfig { build_updater, bin_name }) + }); +} diff --git a/objdiff-gui/src/jobs/check_update.rs b/objdiff-gui/src/jobs/check_update.rs deleted file mode 100644 index 671cd9a..0000000 --- a/objdiff-gui/src/jobs/check_update.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::sync::mpsc::Receiver; - -use anyhow::{Context, Result}; -use self_update::{cargo_crate_version, update::Release}; - -use crate::{ - jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, - update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD}, -}; - -pub struct CheckUpdateResult { - pub update_available: bool, - pub latest_release: Release, - pub found_binary: Option, -} - -fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result> { - update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?; - let updater = build_updater().context("Failed to create release updater")?; - let latest_release = updater.get_latest_release()?; - let update_available = - self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?; - // Find the binary name in the release assets - let found_binary = latest_release - .assets - .iter() - .find(|a| a.name == BIN_NAME_NEW) - .or_else(|| latest_release.assets.iter().find(|a| a.name == BIN_NAME_OLD)) - .map(|a| a.name.clone()); - - update_status(context, "Complete".to_string(), 1, 1, &cancel)?; - Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary })) -} - -pub fn start_check_update(ctx: &egui::Context) -> JobState { - start_job(ctx, "Check for updates", Job::CheckUpdate, move |context, cancel| { - run_check_update(&context, cancel).map(|result| JobResult::CheckUpdate(Some(result))) - }) -} diff --git a/objdiff-gui/src/jobs/objdiff.rs b/objdiff-gui/src/jobs/objdiff.rs deleted file mode 100644 index 0c69597..0000000 --- a/objdiff-gui/src/jobs/objdiff.rs +++ /dev/null @@ -1,328 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - process::Command, - sync::mpsc::Receiver, -}; - -use anyhow::{anyhow, Error, Result}; -use objdiff_core::{ - diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff}, - obj::{read, ObjInfo}, -}; -use time::OffsetDateTime; - -use crate::{ - app::{AppConfig, AppState, ObjectConfig}, - jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, -}; - -pub struct BuildStatus { - pub success: bool, - pub cmdline: String, - pub stdout: String, - pub stderr: String, -} - -impl Default for BuildStatus { - fn default() -> Self { - BuildStatus { - success: true, - cmdline: String::new(), - stdout: String::new(), - stderr: String::new(), - } - } -} - -#[derive(Debug, Clone)] -pub struct BuildConfig { - pub project_dir: Option, - pub custom_make: Option, - pub custom_args: Option>, - #[allow(unused)] - pub selected_wsl_distro: Option, -} - -impl BuildConfig { - pub(crate) fn from_config(config: &AppConfig) -> Self { - Self { - project_dir: config.project_dir.clone(), - custom_make: config.custom_make.clone(), - custom_args: config.custom_args.clone(), - selected_wsl_distro: config.selected_wsl_distro.clone(), - } - } -} - -pub struct ObjDiffConfig { - pub build_config: BuildConfig, - pub build_base: bool, - pub build_target: bool, - pub selected_obj: Option, - pub diff_obj_config: DiffObjConfig, - pub selecting_left: Option, - pub selecting_right: Option, -} - -impl ObjDiffConfig { - pub(crate) fn from_state(state: &AppState) -> Self { - Self { - build_config: BuildConfig::from_config(&state.config), - build_base: state.config.build_base, - build_target: state.config.build_target, - selected_obj: state.config.selected_obj.clone(), - diff_obj_config: state.config.diff_obj_config.clone(), - selecting_left: state.selecting_left.clone(), - selecting_right: state.selecting_right.clone(), - } - } -} - -pub struct ObjDiffResult { - pub first_status: BuildStatus, - pub second_status: BuildStatus, - pub first_obj: Option<(ObjInfo, ObjDiff)>, - pub second_obj: Option<(ObjInfo, ObjDiff)>, - pub time: OffsetDateTime, -} - -pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus { - let Some(cwd) = &config.project_dir else { - return BuildStatus { - success: false, - stderr: "Missing project dir".to_string(), - ..Default::default() - }; - }; - let make = config.custom_make.as_deref().unwrap_or("make"); - let make_args = config.custom_args.as_deref().unwrap_or(&[]); - #[cfg(not(windows))] - let mut command = { - let mut command = Command::new(make); - command.current_dir(cwd).args(make_args).arg(arg); - command - }; - #[cfg(windows)] - let mut command = { - use std::os::windows::process::CommandExt; - - use path_slash::PathExt; - let mut command = if config.selected_wsl_distro.is_some() { - Command::new("wsl") - } else { - Command::new(make) - }; - if let Some(distro) = &config.selected_wsl_distro { - // Strip distro root prefix \\wsl.localhost\{distro} - let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro); - let cwd = match cwd.strip_prefix(wsl_path_prefix) { - Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()), - Err(_) => cwd.to_string_lossy().to_string(), - }; - - command - .arg("--cd") - .arg(cwd) - .arg("-d") - .arg(distro) - .arg("--") - .arg(make) - .args(make_args) - .arg(arg.to_slash_lossy().as_ref()); - } else { - command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref()); - } - command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW); - command - }; - let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned(); - for arg in command.get_args() { - cmdline.push(' '); - cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref()); - } - let output = match command.output() { - Ok(output) => output, - Err(e) => { - return BuildStatus { - success: false, - cmdline, - stdout: Default::default(), - stderr: e.to_string(), - }; - } - }; - // Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy - let stdout = String::from_utf8(output.stdout) - .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()); - let stderr = String::from_utf8(output.stderr) - .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()); - BuildStatus { success: output.status.success(), cmdline, stdout, stderr } -} - -fn run_build( - context: &JobContext, - cancel: Receiver<()>, - mut config: ObjDiffConfig, -) -> Result> { - let obj_config = config.selected_obj.ok_or_else(|| Error::msg("Missing obj path"))?; - // Use the per-object symbol mappings, we don't set mappings globally - config.diff_obj_config.symbol_mappings = MappingConfig { - mappings: obj_config.symbol_mappings, - selecting_left: config.selecting_left, - selecting_right: config.selecting_right, - }; - - let project_dir = config - .build_config - .project_dir - .as_ref() - .ok_or_else(|| Error::msg("Missing project dir"))?; - let target_path_rel = if let Some(target_path) = &obj_config.target_path { - Some(target_path.strip_prefix(project_dir).map_err(|_| { - anyhow!( - "Target path '{}' doesn't begin with '{}'", - target_path.display(), - project_dir.display() - ) - })?) - } else { - None - }; - let base_path_rel = if let Some(base_path) = &obj_config.base_path { - Some(base_path.strip_prefix(project_dir).map_err(|_| { - anyhow!( - "Base path '{}' doesn't begin with '{}'", - base_path.display(), - project_dir.display() - ) - })?) - } else { - None - }; - - let mut total = 1; - if config.build_target && target_path_rel.is_some() { - total += 1; - } - if config.build_base && base_path_rel.is_some() { - total += 1; - } - if target_path_rel.is_some() { - total += 1; - } - if base_path_rel.is_some() { - total += 1; - } - - let mut step_idx = 0; - let mut first_status = match target_path_rel { - Some(target_path_rel) if config.build_target => { - update_status( - context, - format!("Building target {}", target_path_rel.display()), - step_idx, - total, - &cancel, - )?; - step_idx += 1; - run_make(&config.build_config, target_path_rel) - } - _ => BuildStatus::default(), - }; - - let mut second_status = match base_path_rel { - Some(base_path_rel) if config.build_base => { - update_status( - context, - format!("Building base {}", base_path_rel.display()), - step_idx, - total, - &cancel, - )?; - step_idx += 1; - run_make(&config.build_config, base_path_rel) - } - _ => BuildStatus::default(), - }; - - let time = OffsetDateTime::now_utc(); - - let first_obj = match &obj_config.target_path { - Some(target_path) if first_status.success => { - update_status( - context, - format!("Loading target {}", target_path_rel.unwrap().display()), - step_idx, - total, - &cancel, - )?; - step_idx += 1; - match read::read(target_path, &config.diff_obj_config) { - Ok(obj) => Some(obj), - Err(e) => { - first_status = BuildStatus { - success: false, - stdout: format!("Loading object '{}'", target_path.display()), - stderr: format!("{:#}", e), - ..Default::default() - }; - None - } - } - } - Some(_) => { - step_idx += 1; - None - } - _ => None, - }; - - let second_obj = match &obj_config.base_path { - Some(base_path) if second_status.success => { - update_status( - context, - format!("Loading base {}", base_path_rel.unwrap().display()), - step_idx, - total, - &cancel, - )?; - step_idx += 1; - match read::read(base_path, &config.diff_obj_config) { - Ok(obj) => Some(obj), - Err(e) => { - second_status = BuildStatus { - success: false, - stdout: format!("Loading object '{}'", base_path.display()), - stderr: format!("{:#}", e), - ..Default::default() - }; - None - } - } - } - Some(_) => { - step_idx += 1; - None - } - _ => None, - }; - - update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?; - step_idx += 1; - let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?; - - update_status(context, "Complete".to_string(), step_idx, total, &cancel)?; - Ok(Box::new(ObjDiffResult { - first_status, - second_status, - first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))), - second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))), - time, - })) -} - -pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState { - start_job(ctx, "Build", Job::ObjDiff, move |context, cancel| { - run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result))) - }) -} diff --git a/objdiff-gui/src/update.rs b/objdiff-gui/src/update.rs index b2605f2..bc3a346 100644 --- a/objdiff-gui/src/update.rs +++ b/objdiff-gui/src/update.rs @@ -1,5 +1,7 @@ +use anyhow::Result; use cfg_if::cfg_if; use const_format::formatcp; +use objdiff_core::jobs::update::self_update; use self_update::{cargo_crate_version, update::ReleaseUpdate}; pub const OS: &str = std::env::consts::OS; @@ -26,8 +28,8 @@ pub const BIN_NAME_OLD: &str = formatcp!("objdiff-{}-{}{}", OS, ARCH, std::env:: pub const RELEASE_URL: &str = formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO); -pub fn build_updater() -> self_update::errors::Result> { - self_update::backends::github::Update::configure() +pub fn build_updater() -> Result> { + Ok(self_update::backends::github::Update::configure() .repo_owner(GITHUB_USER) .repo_name(GITHUB_REPO) // bin_name is required, but unused? @@ -35,5 +37,5 @@ pub fn build_updater() -> self_update::errors::Result> { .no_confirm(true) .show_output(false) .current_version(cargo_crate_version!()) - .build() + .build()?) } diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index 6d7fd9e..08a04a6 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -15,17 +15,14 @@ use globset::Glob; use objdiff_core::{ config::{ProjectObject, DEFAULT_WATCH_PATTERNS}, diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter}, + jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult}, }; use strum::{EnumMessage, VariantArray}; use crate::{ app::{AppConfig, AppState, AppStateRef, ObjectConfig}, config::ProjectObjectNode, - jobs::{ - check_update::{start_check_update, CheckUpdateResult}, - update::start_update, - Job, JobQueue, JobResult, - }, + jobs::{start_check_update, start_update}, update::RELEASE_URL, views::{ appearance::Appearance, @@ -118,11 +115,11 @@ impl ConfigViewState { if self.queue_check_update { self.queue_check_update = false; - jobs.push_once(Job::CheckUpdate, || start_check_update(ctx)); + start_check_update(ctx, jobs); } if let Some(bin_name) = self.queue_update.take() { - jobs.push_once(Job::Update, || start_update(ctx, bin_name)); + start_update(ctx, jobs, bin_name); } } } diff --git a/objdiff-gui/src/views/jobs.rs b/objdiff-gui/src/views/jobs.rs index f6fc2d5..02fad30 100644 --- a/objdiff-gui/src/views/jobs.rs +++ b/objdiff-gui/src/views/jobs.rs @@ -1,11 +1,9 @@ use std::cmp::Ordering; use egui::{ProgressBar, RichText, Widget}; +use objdiff_core::jobs::{JobQueue, JobStatus}; -use crate::{ - jobs::{JobQueue, JobStatus}, - views::appearance::Appearance, -}; +use crate::views::appearance::Appearance; pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) { if ui.button("Clear").clicked() { diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index cd17eb4..844fffb 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -6,18 +6,16 @@ use egui::{ }; use objdiff_core::{ arch::ObjArch, + build::BuildStatus, diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff}, + jobs::{create_scratch::CreateScratchResult, objdiff::ObjDiffResult, Job, JobQueue, JobResult}, obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef}, }; use regex::{Regex, RegexBuilder}; use crate::{ app::AppStateRef, - jobs::{ - create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult}, - objdiff::{BuildStatus, ObjDiffResult}, - Job, JobQueue, JobResult, - }, + jobs::{is_create_scratch_available, start_create_scratch}, views::{ appearance::Appearance, column_layout::{render_header, render_strips}, @@ -178,7 +176,7 @@ impl DiffViewState { } else { self.source_path_available = false; } - self.scratch_available = CreateScratchConfig::is_available(&state.config); + self.scratch_available = is_create_scratch_available(&state.config); self.object_name = state.config.selected_obj.as_ref().map(|o| o.name.clone()).unwrap_or_default(); } @@ -263,14 +261,7 @@ impl DiffViewState { let Ok(state) = state.read() else { return; }; - match CreateScratchConfig::from_config(&state.config, function_name) { - Ok(config) => { - jobs.push_once(Job::CreateScratch, || start_create_scratch(ctx, config)); - } - Err(err) => { - log::error!("Failed to create scratch config: {err}"); - } - } + start_create_scratch(ctx, jobs, &state, function_name); } DiffViewAction::OpenSourcePath => { let Ok(state) = state.read() else {