Experimental objdiff-cli diff auto-rebuild

This commit is contained in:
Luke Street
2024-10-11 18:37:14 -06:00
parent 10b2a9c129
commit 526e031251
24 changed files with 1587 additions and 1312 deletions
Generated
+29 -5
View File
@@ -1398,6 +1398,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"
@@ -2726,6 +2734,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"
@@ -3071,23 +3091,32 @@ 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",
"yaxpeax-arch",
"yaxpeax-arm",
]
@@ -3113,24 +3142,19 @@ dependencies = [
"font-kit",
"globset",
"log",
"notify",
"objdiff-core",
"open",
"path-slash",
"png",
"pollster 0.4.0",
"regex",
"reqwest",
"rfd",
"rlwinmdec",
"ron",
"self_update",
"serde",
"serde_json",
"shell-escape",
"strum",
"tauri-winres",
"tempfile",
"time",
"tracing-subscriber",
"tracing-wasm",
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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.
File diff suppressed because it is too large Load Diff
+25
View File
@@ -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<()>;
}
+24 -2
View File
@@ -16,8 +16,10 @@ documentation = "https://docs.rs/objdiff-core"
crate-type = ["cdylib", "rlib"]
[features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings"]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings", "build"]
any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
build = ["dep:shell-escape", "dep:path-slash", "dep:winapi", "dep:notify", "dep:notify-debouncer-full", "dep:reqwest", "dep:self_update", "dep:tempfile", "dep:time"]
config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
dwarf = ["dep:gimli"]
mips = ["any-arch", "dep:rabbitizer"]
@@ -25,7 +27,6 @@ ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"]
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
[package.metadata.docs.rs]
@@ -81,6 +82,27 @@ arm-attr = { version = "0.1", optional = true }
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], 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 = { version = "0.13", optional = true }
pbjson-build = { version = "0.7", optional = true }
+106
View File
@@ -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<PathBuf>,
pub custom_make: Option<String>,
pub custom_args: Option<Vec<String>>,
#[allow(unused)]
pub selected_wsl_distro: Option<String>,
}
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 }
}
+75
View File
@@ -0,0 +1,75 @@
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<PathBuf>,
pub left_obj_path: Option<PathBuf>,
pub right_obj_path: Option<PathBuf>,
pub patterns: GlobSet,
}
pub fn create_watcher(
modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
waker: Waker,
) -> notify::Result<Watcher> {
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 mut any_match = false;
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());
any_match = true;
}
}
}
if any_match {
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)
}
+4
View File
@@ -176,6 +176,10 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
];
pub fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
#[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo {
pub path: PathBuf,
+50
View File
@@ -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<Box<dyn ReleaseUpdate>>,
pub bin_names: Vec<String>,
}
pub struct CheckUpdateResult {
pub update_available: bool,
pub latest_release: Release,
pub found_binary: Option<String>,
}
fn run_check_update(
context: &JobContext,
cancel: Receiver<()>,
config: CheckUpdateConfig,
) -> Result<Box<CheckUpdateResult>> {
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)))
})
}
@@ -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)]
@@ -26,38 +22,6 @@ pub struct CreateScratchConfig {
pub preset_id: Option<u32>,
}
impl CreateScratchConfig {
pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result<Self> {
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(),
preset_id: scratch_config.preset_id,
})
}
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,
@@ -99,7 +63,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()))?;
@@ -117,7 +81,7 @@ fn run_create_scratch(
form = form.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))?;
@@ -131,8 +95,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)))
})
@@ -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::<String>() {
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<RwLock<JobStatus>>,
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<JobResult> + 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 || match run(context_inner, rx) {
Ok(state) => state,
@@ -162,7 +203,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 }
}
@@ -184,6 +225,6 @@ fn update_status(
w.status = str;
}
drop(w);
context.egui.request_repaint();
context.waker.wake_by_ref();
Ok(())
}
+199
View File
@@ -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<PathBuf>,
pub base_path: Option<PathBuf>,
pub diff_obj_config: DiffObjConfig,
pub symbol_mappings: SymbolMappings,
pub selecting_left: Option<String>,
pub selecting_right: Option<String>,
}
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<Box<ObjDiffResult>> {
// 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)))
})
}
@@ -3,14 +3,19 @@ use std::{
fs::File,
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<Box<dyn ReleaseUpdate>>,
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<Box<UpdateResult>> {
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, fs::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)
})
}
+4
View File
@@ -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;
#[cfg(feature = "any-arch")]
-13
View File
@@ -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.14"
time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui
@@ -76,18 +74,7 @@ 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]
+20 -97
View File
@@ -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<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
pub struct AppState {
pub config: AppConfig,
pub objects: Vec<ProjectObject>,
@@ -399,7 +392,7 @@ pub struct App {
view_state: ViewState,
state: AppStateRef,
modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>,
watcher: Option<Watcher>,
app_path: Option<PathBuf>,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
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::<String>() {
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<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
) -> notify::Result<notify::RecommendedWatcher> {
let base_dir = project_dir.to_owned();
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| 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) {
+138
View File
@@ -0,0 +1,138 @@
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>) { self.0.request_repaint(); }
fn wake_by_ref(self: &Arc<Self>) { 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<create_scratch::CreateScratchConfig> {
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(),
preset_id: scratch_config.preset_id,
})
}
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 })
});
}
-39
View File
@@ -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<String>,
}
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
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)))
})
}
-328
View File
@@ -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<PathBuf>,
pub custom_make: Option<String>,
pub custom_args: Option<Vec<String>>,
#[allow(unused)]
pub selected_wsl_distro: Option<String>,
}
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<ObjectConfig>,
pub diff_obj_config: DiffObjConfig,
pub selecting_left: Option<String>,
pub selecting_right: Option<String>,
}
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<Box<ObjDiffResult>> {
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)))
})
}

Some files were not shown because too many files have changed in this diff Show More