From 37bc399041166d2f24d081cf6de4c375d64700a3 Mon Sep 17 00:00:00 2001 From: Abel Briggs Date: Mon, 21 Sep 2026 17:13:28 -0700 Subject: [PATCH 1/3] core/jobs: Always wake after running a job If a job runs into an error case before calling `update_status()`, the job will never be considered 'completed' and the polling thread will never wake. Handle this by always waking at the end of the job. --- objdiff-core/src/jobs/mod.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/objdiff-core/src/jobs/mod.rs b/objdiff-core/src/jobs/mod.rs index e3dddd5d..4b08db26 100644 --- a/objdiff-core/src/jobs/mod.rs +++ b/objdiff-core/src/jobs/mod.rs @@ -203,16 +203,22 @@ fn start_job( error: None, })); let context = JobContext { status: status.clone(), waker: waker.clone() }; - let context_inner = JobContext { status: status.clone(), waker }; + let context_inner = JobContext { status: status.clone(), waker: waker.clone() }; let (tx, rx) = std::sync::mpsc::channel(); - let handle = std::thread::spawn(move || match run(context_inner, rx) { - Ok(state) => state, - Err(e) => { - if let Ok(mut w) = status.write() { - w.error = Some(e); + let handle = std::thread::spawn(move || { + let result = match run(context_inner, rx) { + Ok(state) => state, + Err(e) => { + if let Ok(mut w) = status.write() { + w.error = Some(e); + } + JobResult::None } - JobResult::None - } + }; + // Always wake on completion, so the frontend notices jobs that finished + // (or failed) without reporting any progress. + waker.wake(); + result }); let id = JOB_ID.fetch_add(1, Ordering::Relaxed); JobState { id, kind, handle: Some(handle), context, cancel: tx } From a7bdce3bd27defa0ff1d076d4321856b9b904682 Mon Sep 17 00:00:00 2001 From: Abel Briggs Date: Mon, 21 Sep 2026 17:19:45 -0700 Subject: [PATCH 2/3] cli/cmd/diff: Show job failures in the TUI Previously, if a job failed, the terminal would not give any feedback, showing a blank display. Handle this by printing the job status instead. --- objdiff-cli/src/cmd/diff.rs | 22 ++++++++++++++++++++++ objdiff-core/src/build/mod.rs | 1 + 2 files changed, 23 insertions(+) diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index 96fb454f..f4ff5443 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -345,6 +345,28 @@ impl AppState { fn check_jobs(&mut self) -> Result { let mut redraw = false; self.jobs.collect_results(); + // Surface job errors (e.g. a failed build) instead of silently showing nothing. + for job in self.jobs.iter_mut() { + let Some((title, error)) = job + .context + .status + .write() + .ok() + .and_then(|mut s| s.error.take().map(|e| (s.title.clone(), e))) + else { + continue; + }; + let status = BuildStatus { + success: false, + stdout: format!("Job \"{title}\" failed"), + stderr: format!("{error:#}"), + ..Default::default() + }; + self.left_status = Some(status.clone()); + self.right_status = Some(status); + redraw = true; + } + self.jobs.clear_finished(); for result in mem::take(&mut self.jobs.results) { match result { JobResult::None => unreachable!("Unexpected JobResult::None"), diff --git a/objdiff-core/src/build/mod.rs b/objdiff-core/src/build/mod.rs index 248d653d..a88048df 100644 --- a/objdiff-core/src/build/mod.rs +++ b/objdiff-core/src/build/mod.rs @@ -4,6 +4,7 @@ use std::process::Command; use typed_path::{Utf8PlatformPathBuf, Utf8UnixPath}; +#[derive(Clone)] pub struct BuildStatus { pub success: bool, pub cmdline: String, From 828d6f524fb0d45f03aeefa3d636e762ec698796 Mon Sep 17 00:00:00 2001 From: Abel Briggs Date: Mon, 21 Sep 2026 17:23:47 -0700 Subject: [PATCH 3/3] cli/cmd/diff: Route derived `project` dir to interactive TUI The CLI naturally infers the location of the project file/directory via either CLI arguments or the current working directory. `run_interactive()`, however, always populates the project dir from the `-p` argument. If the project dir is inferred via CWD, this fails. Route the inferred project dir from the caller instead. --- objdiff-cli/src/cmd/diff.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index f4ff5443..7cf4edc8 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -81,12 +81,12 @@ pub struct Args { } pub fn run(args: Args) -> Result<()> { - let (target_path, base_path, project_config, unit_options, symbol_mappings) = + let (project_dir, target_path, base_path, project_config, unit_options, symbol_mappings) = match (&args.target, &args.base, &args.project, &args.unit) { (Some(_), Some(_), None, None) | (Some(_), None, None, None) | (None, Some(_), None, None) => { - (args.target.clone(), args.base.clone(), None, None, BTreeMap::new()) + (None, args.target.clone(), args.base.clone(), None, None, BTreeMap::new()) } (None, None, p, u) => { let project = match p { @@ -168,7 +168,14 @@ pub fn run(args: Args) -> Result<()> { let target_path = object.target_path.clone(); let base_path = object.base_path.clone(); let symbol_mappings = object.symbol_mappings.clone(); - (target_path, base_path, Some(project_config), unit_options, symbol_mappings) + ( + Some(project), + target_path, + base_path, + Some(project_config), + unit_options, + symbol_mappings, + ) } _ => bail!("Either target and base or project and unit must be specified"), }; @@ -184,7 +191,15 @@ pub fn run(args: Args) -> Result<()> { &symbol_mappings, ) } else { - run_interactive(args, target_path, base_path, project_config, unit_options, symbol_mappings) + run_interactive( + args, + project_dir, + target_path, + base_path, + project_config, + unit_options, + symbol_mappings, + ) } } @@ -400,6 +415,7 @@ impl Wake for TermWaker { fn run_interactive( args: Args, + project_dir: Option, target_path: Option, base_path: Option, project_config: Option, @@ -418,7 +434,7 @@ fn run_interactive( let mut state = AppState { jobs: Default::default(), waker: Default::default(), - project_dir: args.project.clone(), + project_dir, project_config, target_path, base_path,