Compare commits

..

19 Commits

Author SHA1 Message Date
7b58f9a269 Adjust "Diffable" to exclude missing target objects 2023-10-09 12:47:22 -04:00
d9e7dacb6d Version 0.5.1 2023-10-07 14:49:01 -04:00
04b4fdcd21 Reload objects when changed externally
Uses file modification timestamp polling for project config and objects to avoid unneeded complexity from the filesystem notification watcher.

Allows disabling `build_base` as well for projects using an external build system.
2023-10-07 14:48:34 -04:00
803eaafee6 Hide hidden symbols by default; add "Diff Options" to menu 2023-10-07 13:27:12 -04:00
e1dc84698f Restore context menu on highlightable fields
Fixes #30
2023-10-07 13:04:08 -04:00
e68629c339 Update ppc750cl (subi{,s,c} mnemonics, capstone-style CR bits) 2023-10-06 01:22:26 -04:00
bb9ff4b928 Update all dependencies 2023-10-05 23:55:01 -04:00
57392daaeb Implement click-to-highlight
Highlights registers, instructions, arguments, symbols or addresses on click.

Resolves #7
2023-10-05 23:40:45 -04:00
2dd3dd60a8 Update webpki (advisory fix) 2023-10-05 00:01:09 -04:00
f4757b8d92 Version 0.4.4
Add `#[serde(default)]` to new AppConfig field
2023-10-04 23:52:00 -04:00
52f8c5d4f9 Add "Recent Projects" to file menu 2023-10-03 13:52:16 -04:00
711f40b591 I forgot to bump the Cargo.toml version, oops 2023-09-10 00:24:53 -04:00
26932b2e44 Support min_version field in objdiff.json 2023-09-09 23:54:25 -04:00
192a06bc0b Project configuration improvements
- Support `completed` field for objects in project config. In object tree, displays red for incomplete, green for complete.
- Add support for one-sided diffs. A project can include objects without an associated source file for viewing.
- Add versioning to AppConfig, supporting upgrades without losing user configuration.
2023-09-09 23:43:12 -04:00
5bfa47fce9 Update webpki, rustls-webpki 2023-09-03 09:42:26 -04:00
1d9b9b6893 clippy fix 2023-09-03 09:31:12 -04:00
6b8e469261 Project configuration fixes & improvements
- Allow config to specify object "target_path" and "base_path" explicitly, rather than relying on relative path from the "target_dir" and "base_dir". Useful for more complex directory layouts.
- Fix watch_patterns in project config not using default.
- Fix "Rebuild on changes" not defaulting to true.
- Keep watching project config updates even when "Rebuild on changes" is false.
- Disable some configuration options when loaded from project config file.
2023-09-03 09:28:46 -04:00
bf3ba48539 Match watch_patterns with project-relative paths 2023-08-14 00:21:56 -04:00
21cdf268f0 Update README.md 2023-08-12 14:41:19 -04:00
18 changed files with 1768 additions and 872 deletions

877
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
[package] [package]
name = "objdiff" name = "objdiff"
version = "0.4.0" version = "0.5.2"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff" repository = "https://github.com/encounter/objdiff"
@@ -22,44 +22,47 @@ default = []
wgpu = ["eframe/wgpu"] wgpu = ["eframe/wgpu"]
[dependencies] [dependencies]
anyhow = "1.0.71" anyhow = "1.0.75"
byteorder = "1.4.3" byteorder = "1.5.0"
bytes = "1.4.0" bytes = "1.5.0"
cfg-if = "1.0.0" cfg-if = "1.0.0"
const_format = "0.2.31" const_format = "0.2.31"
cwdemangle = "0.1.5" cwdemangle = "0.1.6"
dirs = "5.0.1" dirs = "5.0.1"
eframe = { version = "0.22.0", features = ["persistence"] } eframe = { version = "0.23.0", features = ["persistence"] }
egui = "0.22.0" egui = "0.23.0"
egui_extras = "0.22.0" egui_extras = "0.23.0"
flagset = "0.4.3" filetime = "0.2.22"
flagset = "0.4.4"
globset = { version = "0.4.13", features = ["serde1"] } globset = { version = "0.4.13", features = ["serde1"] }
log = "0.4.19" log = "0.4.20"
memmap2 = "0.7.1" memmap2 = "0.9.0"
notify = "6.0.1" notify = "6.1.1"
object = { version = "0.31.1", features = ["read_core", "std", "elf"], default-features = false } object = { version = "0.32.1", features = ["read_core", "std", "elf"], default-features = false }
png = "0.17.9" png = "0.17.10"
ppc750cl = { git = "https://github.com/terorie/ppc750cl", rev = "9ae36eef34aa6d74e00972c7671f547a2acfd0aa" } ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "4a2bbbc6f84dcb76255ab6f3595a8d4a0ce96618" }
rabbitizer = "1.7.4" rabbitizer = "1.7.10"
rfd = { version = "0.11.4" } #, default-features = false, features = ['xdg-portal'] rfd = { version = "0.12.0" } #, default-features = false, features = ['xdg-portal']
ron = "0.8.1"
semver = "1.0.19"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1.0.104" serde_json = "1.0.107"
serde_yaml = "0.9.25" serde_yaml = "0.9.25"
tempfile = "3.6.0" tempfile = "3.8.0"
thiserror = "1.0.41" thiserror = "1.0.49"
time = { version = "0.3.22", features = ["formatting", "local-offset"] } time = { version = "0.3.29", features = ["formatting", "local-offset"] }
toml = "0.7.6" toml = "0.8.2"
twox-hash = "1.6.3" twox-hash = "1.6.3"
# For Linux static binaries, use rustls # For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.11.18", default-features = false, features = ["blocking", "json", "rustls"] } reqwest = { version = "0.11.22", default-features = false, features = ["blocking", "json", "rustls"] }
self_update = { version = "0.37.0", default-features = false, features = ["rustls"] } self_update = { version = "0.38.0", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS # For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies] [target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = "0.11.18" reqwest = "0.11.22"
self_update = "0.37.0" self_update = "0.38.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
path-slash = "0.2.1" path-slash = "0.2.1"
@@ -81,5 +84,5 @@ console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2" tracing-wasm = "0.2"
[build-dependencies] [build-dependencies]
anyhow = "1.0.71" anyhow = "1.0.75"
vergen = { version = "8.2.4", features = ["build", "cargo", "git", "gitcl"] } vergen = { version = "8.2.5", features = ["build", "cargo", "git", "gitcl"] }

View File

@@ -16,10 +16,11 @@ See [Usage](#usage) for more information.
## Usage ## Usage
objdiff works by comparing two relocatable object files (`.o`). The objects are expected to have the same relative path from the "target" and "base" directories. objdiff works by comparing two relocatable object files (`.o`). The objects are expected to have the same relative path
from the "target" and "base" directories.
For example, if the target ("expected") object is located at `build/asm/MetroTRK/mslsupp.o` and the base ("actual") object For example, if the target ("expected") object is located at `build/asm/MetroTRK/mslsupp.o` and the base ("actual")
is located at `build/src/MetroTRK/mslsupp.o`, the following configuration would be used: object is located at `build/src/MetroTRK/mslsupp.o`, the following configuration would be used:
- Target build directory: `build/asm` - Target build directory: `build/asm`
- Base build directory: `build/src` - Base build directory: `build/src`
@@ -38,14 +39,22 @@ See [Configuration](#configuration) for more information.
## Configuration ## Configuration
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` (or `objdiff.yaml`, `objdiff.yml`) file to configure the tool automatically. The configuration file must be located in the root project directory. While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` (or
`objdiff.yaml`, `objdiff.yml`) file to configure the tool automatically. The configuration file must be located in
the root project directory.
If your project has a generator script (e.g. `configure.py`), it's recommended to generate the objdiff configuration
file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it from being committed.
```json5 ```json5
// objdiff.json // objdiff.json
{ {
"custom_make": "ninja", "custom_make": "ninja",
"target_dir": "build/mp1.0/asm",
"base_dir": "build/mp1.0/src", // Only required if objects use "path" instead of "target_path" and "base_path".
"target_dir": "build/asm",
"base_dir": "build/src",
"build_target": true, "build_target": true,
"watch_patterns": [ "watch_patterns": [
"*.c", "*.c",
@@ -57,8 +66,15 @@ While **not required** (most settings can be specified in the UI), projects can
], ],
"objects": [ "objects": [
{ {
"name": "main/MetroTRK/mslsupp",
// Option 1: Relative to target_dir and base_dir
"path": "MetroTRK/mslsupp.o", "path": "MetroTRK/mslsupp.o",
"name": "MetroTRK/mslsupp", // Option 2: Explicit paths from project root
// Useful for more complex directory layouts
"target_path": "build/asm/MetroTRK/mslsupp.o",
"base_path": "build/src/MetroTRK/mslsupp.o",
"reverse_fn_order": false "reverse_fn_order": false
}, },
// ... // ...
@@ -66,22 +82,41 @@ While **not required** (most settings can be specified in the UI), projects can
} }
``` ```
- `custom_make` _(optional)_: By default, objdiff will use `make` to build the project. `custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
If the project uses a different build system (e.g. `ninja`), specify it here. If the project uses a different build system (e.g. `ninja`), specify it here.
- `target_dir`: Relative from the root of the project, this where the "target" or "expected" objects are located.
These are the **intended result** of the match. `target_dir` _(optional)_: Relative from the root of the project, this where the "target" or "expected" objects are located.
- `base_dir`: Relative from the root of the project, this is where the "base" or "actual" objects are located. These are the **intended result** of the match.
These are objects built from the **current source code**.
- `build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`). `base_dir` _(optional)_: Relative from the root of the project, this is where the "base" or "actual" objects are located.
This is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files. These are objects built from the **current source code**.
Requires the build system to be configured properly.
- `watch_patterns` _(optional)_: A list of glob patterns to watch for changes. ([Supported syntax](https://docs.rs/globset/latest/globset/#syntax)) `build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g.
If any of these files change, objdiff will automatically rebuild the objects and re-compare them. `make path/to/target.o`).
- `objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation. This is useful if the target objects are not built by default or can change based on project configuration or edits
- `path`: Relative path to the object from the `target_dir` and `base_dir`. to assembly files.
- `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used. Requires the build system to be configured properly.
- `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file. `watch_patterns` _(optional)_: A list of glob patterns to watch for changes.
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
If not specified, objdiff will use the default patterns listed above.
`objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
> `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
>
> `path`: Relative path to the object from the `target_dir` and `base_dir`.
> Requires `target_dir` and `base_dir` to be specified.
>
> `target_path`: Path to the target object from the project root.
> Required if `path` is not specified.
>
> `base_path`: Path to the base object from the project root.
> Required if `path` is not specified.
>
> `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file.
@@ -96,6 +131,5 @@ at your option.
### Contribution ### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
additional terms or conditions.

View File

@@ -1,5 +1,6 @@
use std::{ use std::{
default::Default, default::Default,
fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::{ sync::{
@@ -9,18 +10,21 @@ use std::{
time::Duration, time::Duration,
}; };
use globset::{Glob, GlobSet, GlobSetBuilder}; use filetime::FileTime;
use globset::{Glob, GlobSet};
use notify::{RecursiveMode, Watcher}; use notify::{RecursiveMode, Watcher};
use time::UtcOffset; use time::UtcOffset;
use crate::{ use crate::{
config::{ app_config::{deserialize_config, AppConfigVersion},
build_globset, load_project_config, ProjectObject, ProjectObjectNode, CONFIG_FILENAMES, config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode},
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult, JobStatus,
}, },
jobs::{objdiff::start_build, Job, JobQueue, JobResult, JobStatus},
views::{ views::{
appearance::{appearance_window, Appearance}, appearance::{appearance_window, Appearance},
config::{config_ui, project_window, ConfigViewState}, config::{config_ui, project_window, ConfigViewState, DEFAULT_WATCH_PATTERNS},
data_diff::data_diff_ui, data_diff::data_diff_ui,
demangle::{demangle_window, DemangleViewState}, demangle::{demangle_window, DemangleViewState},
function_diff::function_diff_ui, function_diff::function_diff_ui,
@@ -40,18 +44,60 @@ pub struct ViewState {
pub show_project_config: bool, pub show_project_config: bool,
} }
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)] /// The configuration for a single object file.
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig {
pub name: String,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
}
#[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo {
pub path: PathBuf,
pub timestamp: FileTime,
}
#[inline]
fn bool_true() -> bool { true }
#[inline]
fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfig { pub struct AppConfig {
// TODO: https://github.com/ron-rs/ron/pull/455
// #[serde(flatten)]
// pub version: AppConfigVersion,
pub version: u32,
#[serde(default)]
pub custom_make: Option<String>, pub custom_make: Option<String>,
#[serde(default)]
pub selected_wsl_distro: Option<String>, pub selected_wsl_distro: Option<String>,
#[serde(default)]
pub project_dir: Option<PathBuf>, pub project_dir: Option<PathBuf>,
#[serde(default)]
pub target_obj_dir: Option<PathBuf>, pub target_obj_dir: Option<PathBuf>,
#[serde(default)]
pub base_obj_dir: Option<PathBuf>, pub base_obj_dir: Option<PathBuf>,
pub obj_path: Option<String>, #[serde(default)]
pub selected_obj: Option<ObjectConfig>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool, pub build_target: bool,
pub watcher_enabled: bool, #[serde(default = "bool_true")]
pub rebuild_on_changes: bool,
#[serde(default)]
pub auto_update_check: bool, pub auto_update_check: bool,
#[serde(default = "default_watch_patterns")]
pub watch_patterns: Vec<Glob>, pub watch_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[serde(skip)] #[serde(skip)]
pub objects: Vec<ProjectObject>, pub objects: Vec<ProjectObject>,
@@ -65,14 +111,51 @@ pub struct AppConfig {
pub obj_change: bool, pub obj_change: bool,
#[serde(skip)] #[serde(skip)]
pub queue_build: bool, pub queue_build: bool,
#[serde(skip)]
pub queue_reload: bool,
#[serde(skip)]
pub project_config_info: Option<ProjectConfigInfo>,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
version: AppConfigVersion::default().version,
custom_make: None,
selected_wsl_distro: None,
project_dir: None,
target_obj_dir: None,
base_obj_dir: None,
selected_obj: None,
build_base: true,
build_target: false,
rebuild_on_changes: true,
auto_update_check: true,
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
recent_projects: vec![],
objects: vec![],
object_nodes: vec![],
watcher_change: false,
config_change: false,
obj_change: false,
queue_build: false,
queue_reload: false,
project_config_info: None,
}
}
} }
impl AppConfig { impl AppConfig {
pub fn set_project_dir(&mut self, path: PathBuf) { pub fn set_project_dir(&mut self, path: PathBuf) {
self.recent_projects.retain(|p| p != &path);
if self.recent_projects.len() > 9 {
self.recent_projects.truncate(9);
}
self.recent_projects.insert(0, path.clone());
self.project_dir = Some(path); self.project_dir = Some(path);
self.target_obj_dir = None; self.target_obj_dir = None;
self.base_obj_dir = None; self.base_obj_dir = None;
self.obj_path = None; self.selected_obj = None;
self.build_target = false; self.build_target = false;
self.objects.clear(); self.objects.clear();
self.object_nodes.clear(); self.object_nodes.clear();
@@ -80,24 +163,25 @@ impl AppConfig {
self.config_change = true; self.config_change = true;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.project_config_info = None;
} }
pub fn set_target_obj_dir(&mut self, path: PathBuf) { pub fn set_target_obj_dir(&mut self, path: PathBuf) {
self.target_obj_dir = Some(path); self.target_obj_dir = Some(path);
self.obj_path = None; self.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
} }
pub fn set_base_obj_dir(&mut self, path: PathBuf) { pub fn set_base_obj_dir(&mut self, path: PathBuf) {
self.base_obj_dir = Some(path); self.base_obj_dir = Some(path);
self.obj_path = None; self.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
} }
pub fn set_obj_path(&mut self, path: String) { pub fn set_selected_obj(&mut self, object: ObjectConfig) {
self.obj_path = Some(path); self.selected_obj = Some(object);
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
} }
@@ -111,14 +195,13 @@ pub struct App {
view_state: ViewState, view_state: ViewState,
config: AppConfigRef, config: AppConfigRef,
modified: Arc<AtomicBool>, modified: Arc<AtomicBool>,
config_modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>, watcher: Option<notify::RecommendedWatcher>,
relaunch_path: Rc<Mutex<Option<PathBuf>>>, relaunch_path: Rc<Mutex<Option<PathBuf>>>,
should_relaunch: bool, should_relaunch: bool,
} }
const APPEARANCE_KEY: &str = "appearance"; pub const APPEARANCE_KEY: &str = "appearance";
const CONFIG_KEY: &str = "app_config"; pub const CONFIG_KEY: &str = "app_config";
impl App { impl App {
/// Called once before the first frame. /// Called once before the first frame.
@@ -137,7 +220,7 @@ impl App {
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) { if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
app.appearance = appearance; app.appearance = appearance;
} }
if let Some(mut config) = eframe::get_value::<AppConfig>(storage, CONFIG_KEY) { if let Some(mut config) = deserialize_config(storage) {
if config.project_dir.is_some() { if config.project_dir.is_some() {
config.config_change = true; config.config_change = true;
config.watcher_change = true; config.watcher_change = true;
@@ -217,9 +300,11 @@ impl App {
}; };
let config = &mut *config; let config = &mut *config;
if self.config_modified.swap(false, Ordering::Relaxed) { if let Some(info) = &config.project_config_info {
if file_modified(&info.path, info.timestamp) {
config.config_change = true; config.config_change = true;
} }
}
if config.config_change { if config.config_change {
config.config_change = false; config.config_change = false;
@@ -236,43 +321,57 @@ impl App {
drop(self.watcher.take()); drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir { if let Some(project_dir) = &config.project_dir {
if !config.watch_patterns.is_empty() { match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
match build_globset(&config.watch_patterns) |globset| {
create_watcher(self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)
.and_then(|globset| { },
create_watcher( ) {
self.modified.clone(),
self.config_modified.clone(),
project_dir,
globset,
)
.map_err(anyhow::Error::new)
}) {
Ok(watcher) => self.watcher = Some(watcher), Ok(watcher) => self.watcher = Some(watcher),
Err(e) => log::error!("Failed to create watcher: {e}"), Err(e) => log::error!("Failed to create watcher: {e}"),
} }
}
config.watcher_change = false; config.watcher_change = false;
} }
} }
if config.obj_change { if config.obj_change {
*diff_state = Default::default(); *diff_state = Default::default();
if config.obj_path.is_some() { if config.selected_obj.is_some() {
config.queue_build = true; config.queue_build = true;
} }
config.obj_change = false; config.obj_change = false;
} }
if self.modified.swap(false, Ordering::Relaxed) { if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes {
config.queue_build = true; config.queue_build = true;
} }
if let Some(result) = &diff_state.build {
if let Some(obj) = &result.first_obj {
if file_modified(&obj.path, obj.timestamp) {
config.queue_reload = true;
}
}
if let Some(obj) = &result.second_obj {
if file_modified(&obj.path, obj.timestamp) {
config.queue_reload = true;
}
}
}
// Don't clear `queue_build` if a build is running. A file may have been modified during // Don't clear `queue_build` if a build is running. A file may have been modified during
// the build, so we'll start another build after the current one finishes. // the build, so we'll start another build after the current one finishes.
if config.queue_build && config.obj_path.is_some() && !jobs.is_running(Job::ObjDiff) { if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
jobs.push(start_build(self.config.clone())); jobs.push(start_build(ObjDiffConfig::from_config(config)));
config.queue_build = false; config.queue_build = false;
config.queue_reload = false;
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_config(config);
// Don't build, just reload the current files
diff_config.build_base = false;
diff_config.build_target = false;
jobs.push(start_build(diff_config));
config.queue_reload = false;
} }
} }
} }
@@ -304,6 +403,31 @@ impl eframe::App for App {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
if ui.button("Project…").clicked() {
*show_project_config = !*show_project_config;
ui.close_menu();
}
let recent_projects = if let Ok(guard) = config.read() {
guard.recent_projects.clone()
} else {
vec![]
};
if recent_projects.is_empty() {
ui.add_enabled(false, egui::Button::new("Recent projects…"));
} else {
ui.menu_button("Recent Projects…", |ui| {
if ui.button("Clear").clicked() {
config.write().unwrap().recent_projects.clear();
};
ui.separator();
for path in recent_projects {
if ui.button(format!("{}", path.display())).clicked() {
config.write().unwrap().set_project_dir(path);
ui.close_menu();
}
}
});
}
if ui.button("Appearance…").clicked() { if ui.button("Appearance…").clicked() {
*show_appearance_config = !*show_appearance_config; *show_appearance_config = !*show_appearance_config;
ui.close_menu(); ui.close_menu();
@@ -318,6 +442,29 @@ impl eframe::App for App {
ui.close_menu(); ui.close_menu();
} }
}); });
ui.menu_button("Diff Options", |ui| {
let mut config = config.write().unwrap();
let response = ui
.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes")
.on_hover_text("Automatically re-run the build & diff when files change.");
if response.changed() {
config.watcher_change = true;
};
ui.add_enabled(
!diff_state.symbol_state.disable_reverse_fn_order,
egui::Checkbox::new(
&mut diff_state.symbol_state.reverse_fn_order,
"Reverse function order (-inline deferred)",
),
)
.on_disabled_hover_text(
"Option disabled because it's set by the project configuration file.",
);
ui.checkbox(
&mut diff_state.symbol_state.show_hidden_symbols,
"Show hidden symbols",
);
});
}); });
}); });
@@ -369,24 +516,23 @@ impl eframe::App for App {
fn create_watcher( fn create_watcher(
modified: Arc<AtomicBool>, modified: Arc<AtomicBool>,
config_modified: Arc<AtomicBool>,
project_dir: &Path, project_dir: &Path,
patterns: GlobSet, patterns: GlobSet,
) -> notify::Result<notify::RecommendedWatcher> { ) -> notify::Result<notify::RecommendedWatcher> {
let mut config_patterns = GlobSetBuilder::new(); let base_dir = project_dir.to_owned();
for filename in CONFIG_FILENAMES {
config_patterns.add(Glob::new(&format!("**/{filename}")).unwrap());
}
let config_patterns = config_patterns.build().unwrap();
let mut watcher = let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res { notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => { Ok(event) => {
if matches!(event.kind, notify::EventKind::Modify(..)) { if matches!(
event.kind,
notify::EventKind::Modify(..)
| notify::EventKind::Create(..)
| notify::EventKind::Remove(..)
) {
for path in &event.paths { for path in &event.paths {
if config_patterns.is_match(path) { let Ok(path) = path.strip_prefix(&base_dir) else {
config_modified.store(true, Ordering::Relaxed); continue;
} };
if patterns.is_match(path) { if patterns.is_match(path) {
modified.store(true, Ordering::Relaxed); modified.store(true, Ordering::Relaxed);
} }
@@ -398,3 +544,12 @@ fn create_watcher(
watcher.watch(project_dir, RecursiveMode::Recursive)?; watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher) Ok(watcher)
} }
#[inline]
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
if let Ok(metadata) = fs::metadata(path) {
FileTime::from_last_modification_time(&metadata) != last_ts
} else {
false
}
}

96
src/app_config.rs Normal file
View File

@@ -0,0 +1,96 @@
use std::path::PathBuf;
use eframe::Storage;
use globset::Glob;
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfigVersion {
pub version: u32,
}
impl Default for AppConfigVersion {
fn default() -> Self { Self { version: 1 } }
}
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version {
1 => from_str::<AppConfig>(&str),
_ => {
log::warn!("Unknown config version: {}", version.version);
None
}
},
Err(e) => {
log::warn!("Failed to decode config version: {e}");
// Try to decode as v0
from_str::<AppConfigV0>(&str).map(|c| c.into_config())
}
}
}
fn from_str<T>(str: &str) -> Option<T>
where T: serde::de::DeserializeOwned {
match ron::from_str(str) {
Ok(config) => Some(config),
Err(err) => {
log::warn!("Failed to decode config: {err}");
None
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV0 {
pub name: String,
pub target_path: PathBuf,
pub base_path: PathBuf,
pub reverse_fn_order: Option<bool>,
}
impl ObjectConfigV0 {
fn into_config(self) -> ObjectConfig {
ObjectConfig {
name: self.name,
target_path: Some(self.target_path),
base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order,
complete: None,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct AppConfigV0 {
pub custom_make: Option<String>,
pub selected_wsl_distro: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub selected_obj: Option<ObjectConfigV0>,
pub build_target: bool,
pub auto_update_check: bool,
pub watch_patterns: Vec<Glob>,
}
impl AppConfigV0 {
fn into_config(self) -> AppConfig {
log::info!("Upgrading configuration from v0");
AppConfig {
custom_make: self.custom_make,
selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir,
target_obj_dir: self.target_obj_dir,
base_obj_dir: self.base_obj_dir,
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_target: self.build_target,
auto_update_check: self.auto_update_check,
watch_patterns: self.watch_patterns,
..Default::default()
}
}
}

View File

@@ -1,30 +1,67 @@
use std::{ use std::{
fs::File, fs::File,
io::Read,
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
}; };
use anyhow::{Context, Result}; use anyhow::{bail, Result};
use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
use crate::app::AppConfig; use crate::{
app::{AppConfig, ProjectConfigInfo},
views::config::DEFAULT_WATCH_PATTERNS,
};
#[inline]
fn bool_true() -> bool { true }
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Deserialize)]
#[serde(default)]
pub struct ProjectConfig { pub struct ProjectConfig {
#[serde(default)]
pub min_version: Option<String>,
#[serde(default)]
pub custom_make: Option<String>, pub custom_make: Option<String>,
#[serde(default)]
pub target_dir: Option<PathBuf>, pub target_dir: Option<PathBuf>,
#[serde(default)]
pub base_dir: Option<PathBuf>, pub base_dir: Option<PathBuf>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool, pub build_target: bool,
pub watch_patterns: Vec<Glob>, #[serde(default)]
#[serde(alias = "units")] pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "units")]
pub objects: Vec<ProjectObject>, pub objects: Vec<ProjectObject>,
} }
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectObject { pub struct ProjectObject {
#[serde(default)]
pub name: Option<String>, pub name: Option<String>,
pub path: PathBuf, #[serde(default)]
pub path: Option<PathBuf>,
#[serde(default)]
pub target_path: Option<PathBuf>,
#[serde(default)]
pub base_path: Option<PathBuf>,
#[serde(default)]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)]
pub complete: Option<bool>,
}
impl ProjectObject {
pub fn name(&self) -> &str {
if let Some(name) = &self.name {
name
} else if let Some(path) = &self.path {
path.to_str().unwrap_or("[invalid path]")
} else {
"[unknown]"
}
}
} }
#[derive(Clone)] #[derive(Clone)]
@@ -53,11 +90,22 @@ fn find_dir<'a>(
unreachable!(); unreachable!();
} }
fn build_nodes(objects: &[ProjectObject]) -> Vec<ProjectObjectNode> { fn build_nodes(
objects: &[ProjectObject],
project_dir: &Path,
target_obj_dir: &Option<PathBuf>,
base_obj_dir: &Option<PathBuf>,
) -> Vec<ProjectObjectNode> {
let mut nodes = vec![]; let mut nodes = vec![];
for object in objects { for object in objects {
let mut out_nodes = &mut nodes; let mut out_nodes = &mut nodes;
let path = object.name.as_ref().map(Path::new).unwrap_or(&object.path); let path = if let Some(name) = &object.name {
Path::new(name)
} else if let Some(path) = &object.path {
path
} else {
continue;
};
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
for component in parent.components() { for component in parent.components() {
if let Component::Normal(name) = component { if let Component::Normal(name) = component {
@@ -66,8 +114,23 @@ fn build_nodes(objects: &[ProjectObject]) -> Vec<ProjectObjectNode> {
} }
} }
} }
let mut object = object.clone();
if let (Some(target_obj_dir), Some(path), None) =
(target_obj_dir, &object.path, &object.target_path)
{
object.target_path = Some(target_obj_dir.join(path));
} else if let Some(path) = &object.target_path {
object.target_path = Some(project_dir.join(path));
}
if let (Some(base_obj_dir), Some(path), None) =
(base_obj_dir, &object.path, &object.base_path)
{
object.base_path = Some(base_obj_dir.join(path));
} else if let Some(path) = &object.base_path {
object.base_path = Some(project_dir.join(path));
}
let filename = path.file_name().unwrap().to_str().unwrap().to_string(); let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::File(filename, object.clone())); out_nodes.push(ProjectObjectNode::File(filename, object));
} }
nodes nodes
} }
@@ -78,43 +141,61 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
let Some(project_dir) = &config.project_dir else { let Some(project_dir) = &config.project_dir else {
return Ok(()); return Ok(());
}; };
if let Some(result) = try_project_config(project_dir) { if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?; let project_config = result?;
if let Some(min_version) = &project_config.min_version {
let version_str = env!("CARGO_PKG_VERSION");
let version = semver::Version::parse(version_str).unwrap();
let version_req = semver::VersionReq::parse(&format!(">={min_version}"))?;
if !version_req.matches(&version) {
bail!("Project requires objdiff version {} or higher", min_version);
}
}
config.custom_make = project_config.custom_make; config.custom_make = project_config.custom_make;
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p)); config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p)); config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));
config.build_base = project_config.build_base;
config.build_target = project_config.build_target; config.build_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns; config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
});
config.watcher_change = true; config.watcher_change = true;
config.objects = project_config.objects; config.objects = project_config.objects;
config.object_nodes = build_nodes(&config.objects); config.object_nodes =
build_nodes(&config.objects, project_dir, &config.target_obj_dir, &config.base_obj_dir);
config.project_config_info = Some(info);
} }
Ok(()) Ok(())
} }
fn try_project_config(dir: &Path) -> Option<Result<ProjectConfig>> { fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
for filename in CONFIG_FILENAMES.iter() { for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename); let config_path = dir.join(filename);
if config_path.is_file() { let Ok(mut file) = File::open(&config_path) else {
return match filename.contains("json") { continue;
true => Some(read_json_config(&config_path)),
false => Some(read_yml_config(&config_path)),
}; };
let metadata = file.metadata();
if let Ok(metadata) = metadata {
if !metadata.is_file() {
continue;
}
let ts = FileTime::from_last_modification_time(&metadata);
let config = match filename.contains("json") {
true => read_json_config(&mut file),
false => read_yml_config(&mut file),
};
return Some((config, ProjectConfigInfo { path: config_path, timestamp: ts }));
} }
} }
None None
} }
fn read_yml_config(config_path: &Path) -> Result<ProjectConfig> { fn read_yml_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
let mut reader = File::open(config_path) Ok(serde_yaml::from_reader(reader)?)
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
Ok(serde_yaml::from_reader(&mut reader)?)
} }
fn read_json_config(config_path: &Path) -> Result<ProjectConfig> { fn read_json_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
let mut reader = File::open(config_path) Ok(serde_json::from_reader(reader)?)
.with_context(|| format!("Failed to open config file '{}'", config_path.display()))?;
Ok(serde_json::from_reader(&mut reader)?)
} }
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> { pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {

View File

@@ -372,12 +372,15 @@ fn find_section_and_symbol(obj: &ObjInfo, name: &str) -> Option<(usize, usize)>
None None
} }
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo) -> Result<()> { pub fn diff_objs(mut left: Option<&mut ObjInfo>, mut right: Option<&mut ObjInfo>) -> Result<()> {
if let Some(left) = left.as_mut() {
for left_section in &mut left.sections { for left_section in &mut left.sections {
if left_section.kind == ObjSectionKind::Code { if left_section.kind == ObjSectionKind::Code {
for left_symbol in &mut left_section.symbols { for left_symbol in &mut left_section.symbols {
if let Some((right_section_idx, right_symbol_idx)) = if let Some((right, (right_section_idx, right_symbol_idx))) =
find_section_and_symbol(right, &left_symbol.name) right.as_mut().and_then(|obj| {
find_section_and_symbol(obj, &left_symbol.name).map(|s| (obj, s))
})
{ {
let right_section = &mut right.sections[right_section_idx]; let right_section = &mut right.sections[right_section_idx];
let right_symbol = &mut right_section.symbols[right_symbol_idx]; let right_symbol = &mut right_section.symbols[right_symbol_idx];
@@ -404,21 +407,24 @@ pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo) -> Result<()> {
)?; )?;
} }
} }
} else { } else if let Some(right_section) = right
let Some(right_section) = .as_mut()
right.sections.iter_mut().find(|s| s.name == left_section.name) .and_then(|obj| obj.sections.iter_mut().find(|s| s.name == left_section.name))
else { {
continue;
};
if left_section.kind == ObjSectionKind::Data { if left_section.kind == ObjSectionKind::Data {
diff_data(left_section, right_section); diff_data(left_section, right_section);
// diff_data_symbols(left_section, right_section)?; // diff_data_symbols(left_section, right_section)?;
} else if left_section.kind == ObjSectionKind::Bss { } else if left_section.kind == ObjSectionKind::Bss {
diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?; diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?;
} }
} else if left_section.kind == ObjSectionKind::Data {
no_diff_data(left_section);
} }
} }
for right_section in right.sections.iter_mut().filter(|s| s.kind == ObjSectionKind::Code) { }
if let Some(right) = right.as_mut() {
for right_section in right.sections.iter_mut() {
if right_section.kind == ObjSectionKind::Code {
for right_symbol in &mut right_section.symbols { for right_symbol in &mut right_section.symbols {
if right_symbol.instructions.is_empty() { if right_symbol.instructions.is_empty() {
no_diff_code( no_diff_code(
@@ -430,8 +436,16 @@ pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo) -> Result<()> {
)?; )?;
} }
} }
} else if right_section.kind == ObjSectionKind::Data
&& right_section.data_diff.is_empty()
{
no_diff_data(right_section);
} }
}
}
if let (Some(left), Some(right)) = (left, right) {
diff_bss_symbols(&mut left.common, &mut right.common)?; diff_bss_symbols(&mut left.common, &mut right.common)?;
}
Ok(()) Ok(())
} }
@@ -710,3 +724,12 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
left.data_diff = left_diff; left.data_diff = left_diff;
right.data_diff = right_diff; right.data_diff = right_diff;
} }
fn no_diff_data(section: &mut ObjSection) {
section.data_diff = vec![ObjDataDiff {
data: section.data.clone(),
kind: ObjDataDiffKind::None,
len: section.data.len(),
symbol: String::new(),
}];
}

View File

@@ -1,10 +1,15 @@
use std::{path::Path, process::Command, str::from_utf8, sync::mpsc::Receiver}; use std::{
path::{Path, PathBuf},
process::Command,
str::from_utf8,
sync::mpsc::Receiver,
};
use anyhow::{Context, Error, Result}; use anyhow::{anyhow, Context, Error, Result};
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::{ use crate::{
app::{AppConfig, AppConfigRef}, app::{AppConfig, ObjectConfig},
diff::diff_objs, diff::diff_objs,
jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef}, jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
obj::{elf, ObjInfo}, obj::{elf, ObjInfo},
@@ -15,6 +20,28 @@ pub struct BuildStatus {
pub log: String, pub log: String,
} }
pub struct ObjDiffConfig {
pub build_base: bool,
pub build_target: bool,
pub custom_make: Option<String>,
pub project_dir: Option<PathBuf>,
pub selected_obj: Option<ObjectConfig>,
pub selected_wsl_distro: Option<String>,
}
impl ObjDiffConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self {
ObjDiffConfig {
build_base: config.build_base,
build_target: config.build_target,
custom_make: config.custom_make.clone(),
project_dir: config.project_dir.clone(),
selected_obj: config.selected_obj.clone(),
selected_wsl_distro: config.selected_wsl_distro.clone(),
}
}
}
pub struct ObjDiffResult { pub struct ObjDiffResult {
pub first_status: BuildStatus, pub first_status: BuildStatus,
pub second_status: BuildStatus, pub second_status: BuildStatus,
@@ -23,7 +50,7 @@ pub struct ObjDiffResult {
pub time: OffsetDateTime, pub time: OffsetDateTime,
} }
fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus { fn run_make(cwd: &Path, arg: &Path, config: &ObjDiffConfig) -> BuildStatus {
match (|| -> Result<BuildStatus> { match (|| -> Result<BuildStatus> {
let make = config.custom_make.as_deref().unwrap_or("make"); let make = config.custom_make.as_deref().unwrap_or("make");
#[cfg(not(windows))] #[cfg(not(windows))]
@@ -73,64 +100,113 @@ fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus {
fn run_build( fn run_build(
status: &JobStatusRef, status: &JobStatusRef,
cancel: Receiver<()>, cancel: Receiver<()>,
config: AppConfigRef, config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> { ) -> Result<Box<ObjDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone(); let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
let obj_path = config.obj_path.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
let project_dir = let project_dir =
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?; config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
let mut target_path = config let target_path_rel = if let Some(target_path) = &obj_config.target_path {
.target_obj_dir Some(target_path.strip_prefix(project_dir).map_err(|_| {
.as_ref() anyhow!(
.ok_or_else(|| Error::msg("Missing target obj dir"))? "Target path '{}' doesn't begin with '{}'",
.to_owned(); target_path.display(),
target_path.push(obj_path); project_dir.display()
let mut base_path = )
config.base_obj_dir.as_ref().ok_or_else(|| Error::msg("Missing base obj dir"))?.to_owned(); })?)
base_path.push(obj_path);
let target_path_rel = target_path
.strip_prefix(project_dir)
.context("Failed to create relative target obj path")?;
let base_path_rel =
base_path.strip_prefix(project_dir).context("Failed to create relative base obj path")?;
let total = if config.build_target { 5 } else { 4 };
let first_status = if config.build_target {
update_status(status, format!("Building target {obj_path}"), 0, total, &cancel)?;
run_make(project_dir, target_path_rel, &config)
} else { } else {
BuildStatus { success: true, log: String::new() } 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
}; };
update_status(status, format!("Building base {obj_path}"), 1, total, &cancel)?; let mut total = 3;
let second_status = run_make(project_dir, base_path_rel, &config); if config.build_target && target_path_rel.is_some() {
total += 1;
}
if config.build_base && base_path_rel.is_some() {
total += 1;
}
let first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
status,
format!("Building target {}", target_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(project_dir, target_path_rel, &config)
}
_ => BuildStatus { success: true, log: String::new() },
};
let second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
status,
format!("Building base {}", base_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(project_dir, base_path_rel, &config)
}
_ => BuildStatus { success: true, log: String::new() },
};
let time = OffsetDateTime::now_utc(); let time = OffsetDateTime::now_utc();
let mut first_obj = if first_status.success { let mut first_obj =
update_status(status, format!("Loading target {obj_path}"), 2, total, &cancel)?; match &obj_config.target_path {
Some(elf::read(&target_path)?) Some(target_path) if first_status.success => {
} else { update_status(
None status,
}; format!("Loading target {}", target_path_rel.unwrap().display()),
2,
let mut second_obj = if second_status.success { total,
update_status(status, format!("Loading base {obj_path}"), 3, total, &cancel)?; &cancel,
Some(elf::read(&base_path)?) )?;
} else { Some(elf::read(target_path).with_context(|| {
None format!("Failed to read object '{}'", target_path.display())
}; })?)
if let (Some(first_obj), Some(second_obj)) = (&mut first_obj, &mut second_obj) {
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
diff_objs(first_obj, second_obj)?;
} }
_ => None,
};
let mut second_obj = match &obj_config.base_path {
Some(base_path) if second_status.success => {
update_status(
status,
format!("Loading base {}", base_path_rel.unwrap().display()),
3,
total,
&cancel,
)?;
Some(
elf::read(base_path)
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
)
}
_ => None,
};
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
diff_objs(first_obj.as_mut(), second_obj.as_mut())?;
update_status(status, "Complete".to_string(), total, total, &cancel)?; update_status(status, "Complete".to_string(), total, total, &cancel)?;
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time })) Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))
} }
pub fn start_build(config: AppConfigRef) -> JobState { pub fn start_build(config: ObjDiffConfig) -> JobState {
start_job("Object diff", Job::ObjDiff, move |status, cancel| { start_job("Object diff", Job::ObjDiff, move |status, cancel| {
run_build(status, cancel, config).map(|result| JobResult::ObjDiff(Some(result))) run_build(status, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
}) })

View File

@@ -3,6 +3,7 @@
pub use app::App; pub use app::App;
mod app; mod app;
mod app_config;
mod config; mod config;
mod diff; mod diff;
mod editops; mod editops;

View File

@@ -3,10 +3,11 @@ use std::{collections::BTreeMap, fs, io::Cursor, path::Path};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use byteorder::{BigEndian, ReadBytesExt}; use byteorder::{BigEndian, ReadBytesExt};
use cwdemangle::demangle; use cwdemangle::demangle;
use filetime::FileTime;
use flagset::Flags; use flagset::Flags;
use object::{ use object::{
elf, Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget, elf, Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget,
SectionIndex, SectionKind, Symbol, SymbolKind, SymbolSection, SectionIndex, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
}; };
use crate::obj::{ use crate::obj::{
@@ -42,6 +43,9 @@ fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> R
if symbol.is_weak() { if symbol.is_weak() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak); flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
} }
if symbol.scope() == SymbolScope::Linkage {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
}
let section_address = if let Some(section) = let section_address = if let Some(section) =
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok()) symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
{ {
@@ -301,9 +305,10 @@ fn line_info(obj_file: &File<'_>) -> Result<Option<BTreeMap<u32, u32>>> {
} }
pub fn read(obj_path: &Path) -> Result<ObjInfo> { pub fn read(obj_path: &Path) -> Result<ObjInfo> {
let data = { let (data, timestamp) = {
let file = fs::File::open(obj_path)?; let file = fs::File::open(obj_path)?;
unsafe { memmap2::Mmap::map(&file) }? let timestamp = FileTime::from_last_modification_time(&file.metadata()?);
(unsafe { memmap2::Mmap::map(&file) }?, timestamp)
}; };
let obj_file = File::parse(&*data)?; let obj_file = File::parse(&*data)?;
let architecture = match obj_file.architecture() { let architecture = match obj_file.architecture() {
@@ -319,6 +324,7 @@ pub fn read(obj_path: &Path) -> Result<ObjInfo> {
let mut result = ObjInfo { let mut result = ObjInfo {
architecture, architecture,
path: obj_path.to_owned(), path: obj_path.to_owned(),
timestamp,
sections: filter_sections(&obj_file)?, sections: filter_sections(&obj_file)?,
common: common_symbols(&obj_file)?, common: common_symbols(&obj_file)?,
line_info: line_info(&obj_file)?, line_info: line_info(&obj_file)?,

View File

@@ -91,6 +91,7 @@ pub fn process_code(
reloc: reloc.cloned(), reloc: reloc.cloned(),
branch_dest, branch_dest,
line, line,
orig: None,
}); });
cur_addr += 4; cur_addr += 4;
} }

View File

@@ -4,6 +4,7 @@ pub mod ppc;
use std::{collections::BTreeMap, path::PathBuf}; use std::{collections::BTreeMap, path::PathBuf};
use filetime::FileTime;
use flagset::{flags, FlagSet}; use flagset::{flags, FlagSet};
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
@@ -18,6 +19,7 @@ flags! {
Local, Local,
Weak, Weak,
Common, Common,
Hidden,
} }
} }
#[derive(Debug, Copy, Clone, Default)] #[derive(Debug, Copy, Clone, Default)]
@@ -37,7 +39,8 @@ pub struct ObjSection {
pub data_diff: Vec<ObjDataDiff>, pub data_diff: Vec<ObjDataDiff>,
pub match_percent: f32, pub match_percent: f32,
} }
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ObjInsArg { pub enum ObjInsArg {
PpcArg(ppc750cl::Argument), PpcArg(ppc750cl::Argument),
MipsArg(String), MipsArg(String),
@@ -46,6 +49,7 @@ pub enum ObjInsArg {
RelocWithBase, RelocWithBase,
BranchOffset(i32), BranchOffset(i32),
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct ObjInsArgDiff { pub struct ObjInsArgDiff {
/// Incrementing index for coloring /// Incrementing index for coloring
@@ -86,6 +90,8 @@ pub struct ObjIns {
pub branch_dest: Option<u32>, pub branch_dest: Option<u32>,
/// Line info /// Line info
pub line: Option<u32>, pub line: Option<u32>,
/// Original (unsimplified) instruction
pub orig: Option<String>,
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ObjInsDiff { pub struct ObjInsDiff {
@@ -139,6 +145,7 @@ pub enum ObjArchitecture {
pub struct ObjInfo { pub struct ObjInfo {
pub architecture: ObjArchitecture, pub architecture: ObjArchitecture,
pub path: PathBuf, pub path: PathBuf,
pub timestamp: FileTime,
pub sections: Vec<ObjSection>, pub sections: Vec<ObjSection>,
pub common: Vec<ObjSymbol>, pub common: Vec<ObjSymbol>,
pub line_info: Option<BTreeMap<u32, u32>>, pub line_info: Option<BTreeMap<u32, u32>>,

View File

@@ -1,7 +1,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use anyhow::Result; use anyhow::Result;
use ppc750cl::{disasm_iter, Argument}; use ppc750cl::{disasm_iter, Argument, SimplifiedIns};
use crate::obj::{ObjIns, ObjInsArg, ObjReloc, ObjRelocKind}; use crate::obj::{ObjIns, ObjInsArg, ObjReloc, ObjRelocKind};
@@ -40,7 +40,7 @@ pub fn process_code(
_ => ins.code, _ => ins.code,
}; };
} }
let simplified = ins.simplified(); let simplified = ins.clone().simplified();
let mut args: Vec<ObjInsArg> = simplified let mut args: Vec<ObjInsArg> = simplified
.args .args
.iter() .iter()
@@ -86,9 +86,10 @@ pub fn process_code(
mnemonic: format!("{}{}", simplified.mnemonic, simplified.suffix), mnemonic: format!("{}{}", simplified.mnemonic, simplified.suffix),
args, args,
reloc: reloc.cloned(), reloc: reloc.cloned(),
op: 0, op: ins.op as u8,
branch_dest: None, branch_dest: None,
line, line,
orig: Some(format!("{}", SimplifiedIns::basic_form(ins))),
}); });
} }
Ok((ops, insts)) Ok((ops, insts))

View File

@@ -17,7 +17,7 @@ use globset::Glob;
use self_update::cargo_crate_version; use self_update::cargo_crate_version;
use crate::{ use crate::{
app::{AppConfig, AppConfigRef}, app::{AppConfig, AppConfigRef, ObjectConfig},
config::{ProjectObject, ProjectObjectNode}, config::{ProjectObject, ProjectObjectNode},
jobs::{ jobs::{
check_update::{start_check_update, CheckUpdateResult}, check_update::{start_check_update, CheckUpdateResult},
@@ -40,6 +40,7 @@ pub struct ConfigViewState {
pub watch_pattern_text: String, pub watch_pattern_text: String,
pub load_error: Option<String>, pub load_error: Option<String>,
pub object_search: String, pub object_search: String,
pub filter_diffable: bool,
#[cfg(windows)] #[cfg(windows)]
pub available_wsl_distros: Option<Vec<String>>, pub available_wsl_distros: Option<Vec<String>>,
} }
@@ -79,7 +80,7 @@ impl ConfigViewState {
} }
} }
const DEFAULT_WATCH_PATTERNS: &[&str] = &[ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm", "*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm",
"*.inc", "*.py", "*.yml", "*.txt", "*.json", "*.inc", "*.py", "*.yml", "*.txt", "*.json",
]; ];
@@ -129,7 +130,7 @@ pub fn config_ui(
selected_wsl_distro, selected_wsl_distro,
target_obj_dir, target_obj_dir,
base_obj_dir, base_obj_dir,
obj_path, selected_obj,
auto_update_check, auto_update_check,
objects, objects,
object_nodes, object_nodes,
@@ -205,9 +206,9 @@ pub fn config_ui(
} }
}); });
if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { let mut new_selected_obj = selected_obj.clone();
let mut new_build_obj = obj_path.clone();
if objects.is_empty() { if objects.is_empty() {
if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select object").clicked() { if ui.button("Select object").clicked() {
if let Some(path) = rfd::FileDialog::new() if let Some(path) = rfd::FileDialog::new()
.set_directory(&target_dir) .set_directory(&target_dir)
@@ -215,19 +216,36 @@ pub fn config_ui(
.pick_file() .pick_file()
{ {
if let Ok(obj_path) = path.strip_prefix(&base_dir) { if let Ok(obj_path) = path.strip_prefix(&base_dir) {
new_build_obj = Some(obj_path.display().to_string()); let target_path = target_dir.join(obj_path);
new_selected_obj = Some(ObjectConfig {
name: obj_path.display().to_string(),
target_path: Some(target_path),
base_path: Some(path),
reverse_fn_order: None,
complete: None,
});
} else if let Ok(obj_path) = path.strip_prefix(&target_dir) { } else if let Ok(obj_path) = path.strip_prefix(&target_dir) {
new_build_obj = Some(obj_path.display().to_string()); let base_path = base_dir.join(obj_path);
new_selected_obj = Some(ObjectConfig {
name: obj_path.display().to_string(),
target_path: Some(path),
base_path: Some(base_path),
reverse_fn_order: None,
complete: None,
});
} }
} }
} }
if let Some(obj) = obj_path { if let Some(obj) = selected_obj {
ui.label( ui.label(
RichText::new(&*obj) RichText::new(&obj.name)
.color(appearance.replace_color) .color(appearance.replace_color)
.family(FontFamily::Monospace), .family(FontFamily::Monospace),
); );
} }
} else {
ui.colored_label(appearance.delete_color, "Missing project settings");
}
} else { } else {
let had_search = !state.object_search.is_empty(); let had_search = !state.object_search.is_empty();
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui); egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui);
@@ -244,13 +262,20 @@ pub fn config_ui(
node_open = NodeOpen::Open; node_open = NodeOpen::Open;
} }
if ui if ui
.add_enabled(obj_path.is_some(), egui::Button::new("").small()) .add_enabled(selected_obj.is_some(), egui::Button::new("").small())
.on_hover_text_at_pointer("Current object") .on_hover_text_at_pointer("Current object")
.clicked() .clicked()
{ {
root_open = Some(true); root_open = Some(true);
node_open = NodeOpen::Object; node_open = NodeOpen::Object;
} }
if ui
.selectable_label(state.filter_diffable, "Diffable")
.on_hover_text_at_pointer("Only show objects with a source file")
.clicked()
{
state.filter_diffable = !state.filter_diffable;
}
}); });
if state.object_search.is_empty() { if state.object_search.is_empty() {
if had_search { if had_search {
@@ -270,49 +295,58 @@ pub fn config_ui(
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
let mut nodes = Cow::Borrowed(object_nodes); let mut nodes = Cow::Borrowed(object_nodes);
if !state.object_search.is_empty() { if !state.object_search.is_empty() || state.filter_diffable {
let search = state.object_search.to_ascii_lowercase(); let search = state.object_search.to_ascii_lowercase();
nodes = Cow::Owned( nodes = Cow::Owned(
object_nodes.iter().filter_map(|node| filter_node(node, &search)).collect(), object_nodes
.iter()
.filter_map(|node| filter_node(node, &search, state.filter_diffable))
.collect(),
); );
} }
ui.style_mut().wrap = Some(false); ui.style_mut().wrap = Some(false);
for node in nodes.iter() { for node in nodes.iter() {
display_node(ui, &mut new_build_obj, node, appearance, node_open); display_node(ui, &mut new_selected_obj, node, appearance, node_open);
} }
}); });
} }
if new_selected_obj != *selected_obj {
if new_build_obj != *obj_path { if let Some(obj) = new_selected_obj {
if let Some(obj) = new_build_obj {
// Will set obj_changed, which will trigger a rebuild // Will set obj_changed, which will trigger a rebuild
config_guard.set_obj_path(obj); config_guard.set_selected_obj(obj);
} }
} }
if config_guard.obj_path.is_some() if config_guard.selected_obj.is_some()
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() && ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
{ {
state.queue_build = true; state.queue_build = true;
} }
} else {
ui.colored_label(appearance.delete_color, "Missing project settings");
}
ui.separator(); ui.separator();
} }
fn display_object( fn display_object(
ui: &mut egui::Ui, ui: &mut egui::Ui,
obj_path: &mut Option<String>, selected_obj: &mut Option<ObjectConfig>,
name: &str, name: &str,
object: &ProjectObject, object: &ProjectObject,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let path_string = object.path.to_string_lossy().to_string(); let object_name = object.name();
let selected = matches!(obj_path, Some(path) if path == &path_string); let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
let color = if selected { appearance.emphasized_text_color } else { appearance.text_color }; let color = if selected {
if SelectableLabel::new( appearance.emphasized_text_color
} else if let Some(complete) = object.complete {
if complete {
appearance.insert_color
} else {
appearance.delete_color
}
} else {
appearance.text_color
};
let clicked = SelectableLabel::new(
selected, selected,
RichText::new(name) RichText::new(name)
.font(FontId { .font(FontId {
@@ -322,9 +356,17 @@ fn display_object(
.color(color), .color(color),
) )
.ui(ui) .ui(ui)
.clicked() .clicked();
{ // Always recreate ObjectConfig if selected, in case the project config changed.
*obj_path = Some(path_string); // ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
if selected || clicked {
*selected_obj = Some(ObjectConfig {
name: object_name.to_string(),
target_path: object.target_path.clone(),
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order,
complete: object.complete,
});
} }
} }
@@ -339,17 +381,17 @@ enum NodeOpen {
fn display_node( fn display_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
obj_path: &mut Option<String>, selected_obj: &mut Option<ObjectConfig>,
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::File(name, object) => {
display_object(ui, obj_path, name, object, appearance); display_object(ui, selected_obj, name, object, appearance);
} }
ProjectObjectNode::Dir(name, children) => { ProjectObjectNode::Dir(name, children) => {
let contains_obj = obj_path.as_ref().map(|path| contains_node(node, path)); let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
let open = match node_open { let open = match node_open {
NodeOpen::Default => None, NodeOpen::Default => None,
NodeOpen::Open => Some(true), NodeOpen::Open => Some(true),
@@ -372,40 +414,47 @@ fn display_node(
.open(open) .open(open)
.show(ui, |ui| { .show(ui, |ui| {
for node in children { for node in children {
display_node(ui, obj_path, node, appearance, node_open); display_node(ui, selected_obj, node, appearance, node_open);
} }
}); });
} }
} }
} }
fn contains_node(node: &ProjectObjectNode, path: &str) -> bool { fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool {
match node { match node {
ProjectObjectNode::File(_, object) => { ProjectObjectNode::File(_, object) => object.name() == selected_obj.name,
let path_string = object.path.to_string_lossy().to_string();
path == path_string
}
ProjectObjectNode::Dir(_, children) => { ProjectObjectNode::Dir(_, children) => {
children.iter().any(|node| contains_node(node, path)) children.iter().any(|node| contains_node(node, selected_obj))
} }
} }
} }
fn filter_node(node: &ProjectObjectNode, search: &str) -> Option<ProjectObjectNode> { fn filter_node(
node: &ProjectObjectNode,
search: &str,
filter_diffable: bool,
) -> Option<ProjectObjectNode> {
match node { match node {
ProjectObjectNode::File(name, _) => { ProjectObjectNode::File(name, object) => {
if name.to_ascii_lowercase().contains(search) { if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable
|| (object.base_path.is_some() && object.target_path.is_some()))
{
Some(node.clone()) Some(node.clone())
} else { } else {
None None
} }
} }
ProjectObjectNode::Dir(name, children) => { ProjectObjectNode::Dir(name, children) => {
if name.to_ascii_lowercase().contains(search) { if (search.is_empty() || name.to_ascii_lowercase().contains(search)) && !filter_diffable
{
return Some(node.clone()); return Some(node.clone());
} }
let new_children = let new_children = children
children.iter().filter_map(|child| filter_node(child, search)).collect::<Vec<_>>(); .iter()
.filter_map(|child| filter_node(child, search, filter_diffable))
.collect::<Vec<_>>();
if !new_children.is_empty() { if !new_children.is_empty() {
Some(ProjectObjectNode::Dir(name.clone(), new_children)) Some(ProjectObjectNode::Dir(name.clone(), new_children))
} else { } else {
@@ -444,11 +493,12 @@ fn pick_folder_ui(
label: &str, label: &str,
tooltip: impl FnOnce(&mut egui::Ui), tooltip: impl FnOnce(&mut egui::Ui),
appearance: &Appearance, appearance: &Appearance,
enabled: bool,
) -> egui::Response { ) -> egui::Response {
let response = ui.horizontal(|ui| { let response = ui.horizontal(|ui| {
subheading(ui, label, appearance); subheading(ui, label, appearance);
ui.link(HELP_ICON).on_hover_ui(tooltip); ui.link(HELP_ICON).on_hover_ui(tooltip);
ui.button("Select") ui.add_enabled(enabled, egui::Button::new("Select"))
}); });
ui.label(format_path(dir, appearance)); ui.label(format_path(dir, appearance));
response.inner response.inner
@@ -506,6 +556,7 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
true,
); );
if response.clicked() { if response.clicked() {
if let Some(path) = rfd::FileDialog::new().pick_folder() { if let Some(path) = rfd::FileDialog::new().pick_folder() {
@@ -566,6 +617,7 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
config.project_config_info.is_none(),
); );
if response.clicked() { if response.clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
@@ -615,18 +667,42 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
config.project_config_info.is_none(),
); );
if response.clicked() { if response.clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
config.set_base_obj_dir(path); config.set_base_obj_dir(path);
} }
} }
ui.checkbox(&mut config.build_base, "Build base objects").on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append(
"Tells the build system to produce the base object.\n",
0.0,
text_format.clone(),
);
job.append("For example, this would call ", 0.0, text_format.clone());
job.append("make path/to/base.o", 0.0, code_format.clone());
job.append(".\n\n", 0.0, text_format.clone());
job.append(
"This can be disabled if you're running the build system\n",
0.0,
text_format.clone(),
);
job.append(
"externally, and just want objdiff to reload the files\n",
0.0,
text_format.clone(),
);
job.append("when they change.", 0.0, text_format.clone());
ui.label(job);
});
ui.separator(); ui.separator();
} }
subheading(ui, "Watch settings", appearance); subheading(ui, "Watch settings", appearance);
let response = let response =
ui.checkbox(&mut config.watcher_enabled, "Rebuild on changes").on_hover_ui(|ui| { ui.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
job.append( job.append(
"Automatically re-run the build & diff when files change.", "Automatically re-run the build & diff when files change.",

View File

@@ -132,31 +132,39 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
fn data_table_ui( fn data_table_ui(
table: TableBuilder<'_>, table: TableBuilder<'_>,
left_obj: &ObjInfo, left_obj: Option<&ObjInfo>,
right_obj: &ObjInfo, right_obj: Option<&ObjInfo>,
selected_symbol: &SymbolReference, selected_symbol: &SymbolReference,
config: &Appearance, config: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_section = find_section(left_obj, selected_symbol)?; let left_section = left_obj.and_then(|obj| find_section(obj, selected_symbol));
let right_section = find_section(right_obj, selected_symbol)?; let right_section = right_obj.and_then(|obj| find_section(obj, selected_symbol));
let total_bytes = left_section.data_diff.iter().fold(0usize, |accum, item| accum + item.len); let total_bytes = left_section
.or(right_section)?
.data_diff
.iter()
.fold(0usize, |accum, item| accum + item.len);
if total_bytes == 0 { if total_bytes == 0 {
return None; return None;
} }
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1; let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
let left_diffs = split_diffs(&left_section.data_diff); let left_diffs = left_section.map(|section| split_diffs(&section.data_diff));
let right_diffs = split_diffs(&right_section.data_diff); let right_diffs = right_section.map(|section| split_diffs(&section.data_diff));
table.body(|body| { table.body(|body| {
body.rows(config.code_font.size, total_rows, |row_index, mut row| { body.rows(config.code_font.size, total_rows, |row_index, mut row| {
let address = row_index * BYTES_PER_ROW; let address = row_index * BYTES_PER_ROW;
row.col(|ui| { row.col(|ui| {
if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, address, &left_diffs[row_index], config); data_row_ui(ui, address, &left_diffs[row_index], config);
}
}); });
row.col(|ui| { row.col(|ui| {
if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, address, &right_diffs[row_index], config); data_row_ui(ui, address, &right_diffs[row_index], config);
}
}); });
}); });
}); });
@@ -243,7 +251,6 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
ui.separator(); ui.separator();
// Table // Table
if let (Some(left_obj), Some(right_obj)) = (&result.first_obj, &result.second_obj) {
let available_height = ui.available_height(); let available_height = ui.available_height();
let table = TableBuilder::new(ui) let table = TableBuilder::new(ui)
.striped(false) .striped(false)
@@ -252,6 +259,11 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
.resizable(false) .resizable(false)
.auto_shrink([false, false]) .auto_shrink([false, false])
.min_scrolled_height(available_height); .min_scrolled_height(available_height);
data_table_ui(table, left_obj, right_obj, selected_symbol, appearance); data_table_ui(
} table,
result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
);
} }

View File

@@ -1,9 +1,12 @@
use std::{cmp::Ordering, default::Default}; use std::{
cmp::{max, Ordering},
default::Default,
};
use cwdemangle::demangle; use cwdemangle::demangle;
use eframe::emath::Align; use eframe::emath::Align;
use egui::{text::LayoutJob, Color32, FontId, Label, Layout, Sense, Vec2}; use egui::{text::LayoutJob, Color32, Label, Layout, RichText, Sense, TextFormat, Vec2};
use egui_extras::{Column, TableBuilder}; use egui_extras::{Column, TableBuilder, TableRow};
use ppc750cl::Argument; use ppc750cl::Argument;
use time::format_description; use time::format_description;
@@ -19,21 +22,49 @@ use crate::{
}, },
}; };
#[derive(Default)]
pub enum HighlightKind {
#[default]
None,
Opcode(u8),
Arg(ObjInsArg),
Symbol(String),
Address(u32),
}
#[derive(Default)]
pub struct FunctionViewState {
pub highlight: HighlightKind,
}
fn write_reloc_name( fn write_reloc_name(
reloc: &ObjReloc, reloc: &ObjReloc,
color: Color32, color: Color32,
background_color: Color32,
job: &mut LayoutJob, job: &mut LayoutJob,
font_id: FontId,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name); let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name);
write_text(name, appearance.emphasized_text_color, job, font_id.clone()); job.append(name, 0.0, TextFormat {
font_id: appearance.code_font.clone(),
color: appearance.emphasized_text_color,
background: background_color,
..Default::default()
});
match reloc.target.addend.cmp(&0i64) { match reloc.target.addend.cmp(&0i64) {
Ordering::Greater => { Ordering::Greater => write_text(
write_text(&format!("+{:#X}", reloc.target.addend), color, job, font_id) &format!("+{:#X}", reloc.target.addend),
} color,
job,
appearance.code_font.clone(),
),
Ordering::Less => { Ordering::Less => {
write_text(&format!("-{:#X}", -reloc.target.addend), color, job, font_id); write_text(
&format!("-{:#X}", -reloc.target.addend),
color,
job,
appearance.code_font.clone(),
);
} }
_ => {} _ => {}
} }
@@ -42,57 +73,57 @@ fn write_reloc_name(
fn write_reloc( fn write_reloc(
reloc: &ObjReloc, reloc: &ObjReloc,
color: Color32, color: Color32,
background_color: Color32,
job: &mut LayoutJob, job: &mut LayoutJob,
font_id: FontId,
appearance: &Appearance, appearance: &Appearance,
) { ) {
match reloc.kind { match reloc.kind {
ObjRelocKind::PpcAddr16Lo => { ObjRelocKind::PpcAddr16Lo => {
write_reloc_name(reloc, color, job, font_id.clone(), appearance); write_reloc_name(reloc, color, background_color, job, appearance);
write_text("@l", color, job, font_id); write_text("@l", color, job, appearance.code_font.clone());
} }
ObjRelocKind::PpcAddr16Hi => { ObjRelocKind::PpcAddr16Hi => {
write_reloc_name(reloc, color, job, font_id.clone(), appearance); write_reloc_name(reloc, color, background_color, job, appearance);
write_text("@h", color, job, font_id); write_text("@h", color, job, appearance.code_font.clone());
} }
ObjRelocKind::PpcAddr16Ha => { ObjRelocKind::PpcAddr16Ha => {
write_reloc_name(reloc, color, job, font_id.clone(), appearance); write_reloc_name(reloc, color, background_color, job, appearance);
write_text("@ha", color, job, font_id); write_text("@ha", color, job, appearance.code_font.clone());
} }
ObjRelocKind::PpcEmbSda21 => { ObjRelocKind::PpcEmbSda21 => {
write_reloc_name(reloc, color, job, font_id.clone(), appearance); write_reloc_name(reloc, color, background_color, job, appearance);
write_text("@sda21", color, job, font_id); write_text("@sda21", color, job, appearance.code_font.clone());
} }
ObjRelocKind::MipsHi16 => { ObjRelocKind::MipsHi16 => {
write_text("%hi(", color, job, font_id.clone()); write_text("%hi(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance); write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, font_id); write_text(")", color, job, appearance.code_font.clone());
} }
ObjRelocKind::MipsLo16 => { ObjRelocKind::MipsLo16 => {
write_text("%lo(", color, job, font_id.clone()); write_text("%lo(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance); write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, font_id); write_text(")", color, job, appearance.code_font.clone());
} }
ObjRelocKind::MipsGot16 => { ObjRelocKind::MipsGot16 => {
write_text("%got(", color, job, font_id.clone()); write_text("%got(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance); write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, font_id); write_text(")", color, job, appearance.code_font.clone());
} }
ObjRelocKind::MipsCall16 => { ObjRelocKind::MipsCall16 => {
write_text("%call16(", color, job, font_id.clone()); write_text("%call16(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance); write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, font_id); write_text(")", color, job, appearance.code_font.clone());
} }
ObjRelocKind::MipsGpRel16 => { ObjRelocKind::MipsGpRel16 => {
write_text("%gp_rel(", color, job, font_id.clone()); write_text("%gp_rel(", color, job, appearance.code_font.clone());
write_reloc_name(reloc, color, job, font_id.clone(), appearance); write_reloc_name(reloc, color, background_color, job, appearance);
write_text(")", color, job, font_id); write_text(")", color, job, appearance.code_font.clone());
} }
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 | ObjRelocKind::Mips26 => { ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 | ObjRelocKind::Mips26 => {
write_reloc_name(reloc, color, job, font_id, appearance); write_reloc_name(reloc, color, background_color, job, appearance);
} }
ObjRelocKind::Absolute | ObjRelocKind::MipsGpRel32 => { ObjRelocKind::Absolute | ObjRelocKind::MipsGpRel32 => {
write_text("[INVALID]", color, job, font_id); write_text("[INVALID]", color, job, appearance.code_font.clone());
} }
}; };
} }
@@ -102,8 +133,9 @@ fn write_ins(
diff_kind: &ObjInsDiffKind, diff_kind: &ObjInsDiffKind,
args: &[Option<ObjInsArgDiff>], args: &[Option<ObjInsArgDiff>],
base_addr: u32, base_addr: u32,
job: &mut LayoutJob, ui: &mut egui::Ui,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) { ) {
let base_color = match diff_kind { let base_color = match diff_kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
@@ -113,49 +145,96 @@ fn write_ins(
ObjInsDiffKind::Delete => appearance.delete_color, ObjInsDiffKind::Delete => appearance.delete_color,
ObjInsDiffKind::Insert => appearance.insert_color, ObjInsDiffKind::Insert => appearance.insert_color,
}; };
write_text(
&format!("{:<11}", ins.mnemonic), let highlighted_op =
matches!(ins_view_state.highlight, HighlightKind::Opcode(op) if op == ins.op);
let op_label = RichText::new(ins.mnemonic.clone())
.font(appearance.code_font.clone())
.color(if highlighted_op {
appearance.emphasized_text_color
} else {
match diff_kind { match diff_kind {
ObjInsDiffKind::OpMismatch => appearance.replace_color, ObjInsDiffKind::OpMismatch => appearance.replace_color,
_ => base_color, _ => base_color,
}, }
job, })
appearance.code_font.clone(), .background_color(if highlighted_op {
); appearance.deemphasized_text_color
} else {
Color32::TRANSPARENT
});
if ui
.add(Label::new(op_label).sense(Sense::click()))
.context_menu(|ui| ins_context_menu(ui, ins))
.clicked()
{
if highlighted_op {
ins_view_state.highlight = HighlightKind::None;
} else {
ins_view_state.highlight = HighlightKind::Opcode(ins.op);
}
}
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
ui.add_space(space_width * (max(11, ins.mnemonic.len()) - ins.mnemonic.len()) as f32);
let mut writing_offset = false; let mut writing_offset = false;
for (i, arg) in ins.args.iter().enumerate() { for (i, arg) in ins.args.iter().enumerate() {
let mut job = LayoutJob::default();
if i == 0 { if i == 0 {
write_text(" ", base_color, job, appearance.code_font.clone()); write_text(" ", base_color, &mut job, appearance.code_font.clone());
} }
if i > 0 && !writing_offset { if i > 0 && !writing_offset {
write_text(", ", base_color, job, appearance.code_font.clone()); write_text(", ", base_color, &mut job, appearance.code_font.clone());
} }
let color = if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) { let highlighted_arg = match &ins_view_state.highlight {
HighlightKind::Symbol(v) => {
matches!(arg, ObjInsArg::Reloc | ObjInsArg::RelocWithBase)
&& matches!(&ins.reloc, Some(reloc) if &reloc.target.name == v)
}
HighlightKind::Address(v) => {
matches!(arg, ObjInsArg::BranchOffset(offset) if (offset + ins.address as i32 - base_addr as i32) as u32 == *v)
}
HighlightKind::Arg(v) => v == arg,
_ => false,
};
let color = if highlighted_arg {
appearance.emphasized_text_color
} else if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) {
appearance.diff_colors[diff.idx % appearance.diff_colors.len()] appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
} else { } else {
base_color base_color
}; };
let text_format = TextFormat {
font_id: appearance.code_font.clone(),
color,
background: if highlighted_arg {
appearance.deemphasized_text_color
} else {
Color32::TRANSPARENT
},
..Default::default()
};
let mut new_writing_offset = false;
match arg { match arg {
ObjInsArg::PpcArg(arg) => match arg { ObjInsArg::PpcArg(arg) => match arg {
Argument::Offset(val) => { Argument::Offset(val) => {
write_text(&format!("{val}"), color, job, appearance.code_font.clone()); job.append(&format!("{val}"), 0.0, text_format);
write_text("(", base_color, job, appearance.code_font.clone()); write_text("(", base_color, &mut job, appearance.code_font.clone());
writing_offset = true; new_writing_offset = true;
continue;
} }
Argument::Uimm(_) | Argument::Simm(_) => { Argument::Uimm(_) | Argument::Simm(_) => {
write_text(&format!("{arg}"), color, job, appearance.code_font.clone()); job.append(&format!("{arg}"), 0.0, text_format);
} }
_ => { _ => {
write_text(&format!("{arg}"), color, job, appearance.code_font.clone()); job.append(&format!("{arg}"), 0.0, text_format);
} }
}, },
ObjInsArg::Reloc => { ObjInsArg::Reloc => {
write_reloc( write_reloc(
ins.reloc.as_ref().unwrap(), ins.reloc.as_ref().unwrap(),
base_color, base_color,
job, text_format.background,
appearance.code_font.clone(), &mut job,
appearance, appearance,
); );
} }
@@ -163,41 +242,46 @@ fn write_ins(
write_reloc( write_reloc(
ins.reloc.as_ref().unwrap(), ins.reloc.as_ref().unwrap(),
base_color, base_color,
job, text_format.background,
appearance.code_font.clone(), &mut job,
appearance, appearance,
); );
write_text("(", base_color, job, appearance.code_font.clone()); write_text("(", base_color, &mut job, appearance.code_font.clone());
writing_offset = true; new_writing_offset = true;
continue;
} }
ObjInsArg::MipsArg(str) => { ObjInsArg::MipsArg(str) => {
write_text( job.append(str.strip_prefix('$').unwrap_or(str), 0.0, text_format);
str.strip_prefix('$').unwrap_or(str),
color,
job,
appearance.code_font.clone(),
);
} }
ObjInsArg::MipsArgWithBase(str) => { ObjInsArg::MipsArgWithBase(str) => {
write_text( job.append(str.strip_prefix('$').unwrap_or(str), 0.0, text_format);
str.strip_prefix('$').unwrap_or(str), write_text("(", base_color, &mut job, appearance.code_font.clone());
color, new_writing_offset = true;
job,
appearance.code_font.clone(),
);
write_text("(", base_color, job, appearance.code_font.clone());
writing_offset = true;
continue;
} }
ObjInsArg::BranchOffset(offset) => { ObjInsArg::BranchOffset(offset) => {
let addr = offset + ins.address as i32 - base_addr as i32; let addr = offset + ins.address as i32 - base_addr as i32;
write_text(&format!("{addr:x}"), color, job, appearance.code_font.clone()); job.append(&format!("{addr:x}"), 0.0, text_format);
} }
} }
if writing_offset { if writing_offset {
write_text(")", base_color, job, appearance.code_font.clone()); write_text(")", base_color, &mut job, appearance.code_font.clone());
writing_offset = false; }
writing_offset = new_writing_offset;
if ui
.add(Label::new(job).sense(Sense::click()))
.context_menu(|ui| ins_context_menu(ui, ins))
.clicked()
{
if highlighted_arg {
ins_view_state.highlight = HighlightKind::None;
} else if matches!(arg, ObjInsArg::Reloc | ObjInsArg::RelocWithBase) {
ins_view_state.highlight =
HighlightKind::Symbol(ins.reloc.as_ref().unwrap().target.name.clone());
} else if let ObjInsArg::BranchOffset(offset) = arg {
ins_view_state.highlight =
HighlightKind::Address((offset + ins.address as i32 - base_addr as i32) as u32);
} else {
ins_view_state.highlight = HighlightKind::Arg(arg.clone());
}
} }
} }
} }
@@ -209,6 +293,10 @@ fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns, appearance: &Appearance) {
ui.label(format!("{:02X?}", ins.code.to_be_bytes())); ui.label(format!("{:02X?}", ins.code.to_be_bytes()));
if let Some(orig) = &ins.orig {
ui.label(format!("Original: {}", orig));
}
for arg in &ins.args { for arg in &ins.args {
if let ObjInsArg::PpcArg(arg) = arg { if let ObjInsArg::PpcArg(arg) = arg {
match arg { match arg {
@@ -316,7 +404,9 @@ fn asm_row_ui(
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
symbol: &ObjSymbol, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) { ) {
ui.spacing_mut().item_spacing.x = 0.0;
if ins_diff.kind != ObjInsDiffKind::None { if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color); ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
} }
@@ -345,60 +435,134 @@ fn asm_row_ui(
); );
pad = 12 - line_str.len(); pad = 12 - line_str.len();
} }
write_text( let base_addr = symbol.address as u32;
&format!("{:<1$}", format!("{:x}: ", ins.address - symbol.address as u32), pad), let addr_highlight = matches!(
base_color, &ins_view_state.highlight,
&mut job, HighlightKind::Address(v) if *v == (ins.address - base_addr)
appearance.code_font.clone(),
);
if let Some(branch) = &ins_diff.branch_from {
write_text(
"~> ",
appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
&mut job,
appearance.code_font.clone(),
); );
let addr_string = format!("{:x}", ins.address - symbol.address as u32);
pad -= addr_string.len();
job.append(&addr_string, 0.0, TextFormat {
font_id: appearance.code_font.clone(),
color: if addr_highlight { appearance.emphasized_text_color } else { base_color },
background: if addr_highlight {
appearance.deemphasized_text_color
} else { } else {
write_text(" ", base_color, &mut job, appearance.code_font.clone()); Color32::TRANSPARENT
},
..Default::default()
});
if ui
.add(Label::new(job).sense(Sense::click()))
.context_menu(|ui| ins_context_menu(ui, ins))
.clicked()
{
if addr_highlight {
ins_view_state.highlight = HighlightKind::None;
} else {
ins_view_state.highlight = HighlightKind::Address(ins.address - base_addr);
} }
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job, appearance); }
let mut job = LayoutJob::default();
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
let spacing = space_width * pad as f32;
job.append(": ", 0.0, TextFormat {
font_id: appearance.code_font.clone(),
color: base_color,
..Default::default()
});
if let Some(branch) = &ins_diff.branch_from {
job.append("~> ", spacing, TextFormat {
font_id: appearance.code_font.clone(),
color: appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
..Default::default()
});
} else {
job.append(" ", spacing, TextFormat {
font_id: appearance.code_font.clone(),
color: base_color,
..Default::default()
});
}
ui.add(Label::new(job));
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, base_addr, ui, appearance, ins_view_state);
if let Some(branch) = &ins_diff.branch_to { if let Some(branch) = &ins_diff.branch_to {
let mut job = LayoutJob::default();
write_text( write_text(
" ~>", " ~>",
appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()], appearance.diff_colors[branch.branch_idx % appearance.diff_colors.len()],
&mut job, &mut job,
appearance.code_font.clone(), appearance.code_font.clone(),
); );
ui.add(Label::new(job));
} }
ui.add(Label::new(job).sense(Sense::click())) }
.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins, appearance))
.context_menu(|ui| ins_context_menu(ui, ins)); fn asm_col_ui(
row: &mut TableRow<'_, '_>,
ins_diff: &ObjInsDiff,
symbol: &ObjSymbol,
appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) {
let (_, response) = row.col(|ui| {
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state);
});
if let Some(ins) = &ins_diff.ins {
response
.on_hover_ui_at_pointer(|ui| {
ins_hover_ui(ui, ins, appearance);
})
.context_menu(|ui| {
ins_context_menu(ui, ins);
});
}
}
fn empty_col_ui(row: &mut TableRow<'_, '_>) {
row.col(|ui| {
ui.label("");
});
} }
fn asm_table_ui( fn asm_table_ui(
table: TableBuilder<'_>, table: TableBuilder<'_>,
left_obj: &ObjInfo, left_obj: Option<&ObjInfo>,
right_obj: &ObjInfo, right_obj: Option<&ObjInfo>,
selected_symbol: &SymbolReference, selected_symbol: &SymbolReference,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState,
) -> Option<()> { ) -> Option<()> {
let left_symbol = find_symbol(left_obj, selected_symbol); let left_symbol = left_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let right_symbol = find_symbol(right_obj, selected_symbol); let right_symbol = right_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?; let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?;
table.body(|body| { table.body(|body| {
body.rows(appearance.code_font.size, instructions_len, |row_index, mut row| { body.rows(appearance.code_font.size, instructions_len, |row_index, mut row| {
row.col(|ui| {
if let Some(symbol) = left_symbol { if let Some(symbol) = left_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, appearance); asm_col_ui(
&mut row,
&symbol.instructions[row_index],
symbol,
appearance,
ins_view_state,
);
} else {
empty_col_ui(&mut row);
} }
});
row.col(|ui| {
if let Some(symbol) = right_symbol { if let Some(symbol) = right_symbol {
asm_row_ui(ui, &symbol.instructions[row_index], symbol, appearance); asm_col_ui(
&mut row,
&symbol.instructions[row_index],
symbol,
appearance,
ins_view_state,
);
} else {
empty_col_ui(&mut row);
} }
}); });
}); });
});
Some(()) Some(())
} }
@@ -492,7 +656,7 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
&format!("{match_percent:.0}%"), &format!("{match_percent:.0}%"),
); );
} else { } else {
ui.label(""); ui.colored_label(appearance.replace_color, "Missing");
} }
ui.label("Diff base:"); ui.label("Diff base:");
}); });
@@ -503,7 +667,6 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
ui.separator(); ui.separator();
// Table // Table
if let (Some(left_obj), Some(right_obj)) = (&result.first_obj, &result.second_obj) {
let available_height = ui.available_height(); let available_height = ui.available_height();
let table = TableBuilder::new(ui) let table = TableBuilder::new(ui)
.striped(false) .striped(false)
@@ -512,6 +675,12 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
.resizable(false) .resizable(false)
.auto_shrink([false, false]) .auto_shrink([false, false])
.min_scrolled_height(available_height); .min_scrolled_height(available_height);
asm_table_ui(table, left_obj, right_obj, selected_symbol, appearance); asm_table_ui(
} table,
result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
&mut state.function_state,
);
} }

View File

@@ -1,4 +1,4 @@
use egui::{ProgressBar, Widget}; use egui::{ProgressBar, RichText, Widget};
use crate::{jobs::JobQueue, views::appearance::Appearance}; use crate::{jobs::JobQueue, views::appearance::Appearance};
@@ -31,7 +31,7 @@ pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance)
bar.ui(ui); bar.ui(ui);
const STATUS_LENGTH: usize = 80; const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error { if let Some(err) = &status.error {
let err_string = err.to_string(); let err_string = format!("{:#}", err);
ui.colored_label( ui.colored_label(
appearance.delete_color, appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 { if err_string.len() > STATUS_LENGTH - 10 {
@@ -39,13 +39,15 @@ pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance)
} else { } else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7) format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
}, },
); )
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
} else { } else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 { ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}", &status.status[0..STATUS_LENGTH - 3]) format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else { } else {
format!("{:width$}", &status.status, width = STATUS_LENGTH) format!("{:width$}", &status.status, width = STATUS_LENGTH)
}); })
.on_hover_text_at_pointer(&status.status);
} }
}); });
} }

View File

@@ -1,7 +1,7 @@
use std::mem::take; use std::mem::take;
use egui::{ use egui::{
text::LayoutJob, Align, CollapsingHeader, Color32, Layout, Rgba, ScrollArea, SelectableLabel, text::LayoutJob, Align, CollapsingHeader, Color32, Layout, ScrollArea, SelectableLabel,
TextEdit, Ui, Vec2, Widget, TextEdit, Ui, Vec2, Widget,
}; };
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
@@ -13,7 +13,7 @@ use crate::{
Job, JobQueue, JobResult, Job, JobQueue, JobResult,
}, },
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags}, obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags},
views::{appearance::Appearance, write_text}, views::{appearance::Appearance, function_diff::FunctionViewState, write_text},
}; };
pub struct SymbolReference { pub struct SymbolReference {
@@ -35,6 +35,7 @@ pub struct DiffViewState {
pub build: Option<Box<ObjDiffResult>>, pub build: Option<Box<ObjDiffResult>>,
pub current_view: View, pub current_view: View,
pub symbol_state: SymbolViewState, pub symbol_state: SymbolViewState,
pub function_state: FunctionViewState,
pub search: String, pub search: String,
pub queue_build: bool, pub queue_build: bool,
pub build_running: bool, pub build_running: bool,
@@ -46,6 +47,7 @@ pub struct SymbolViewState {
pub selected_symbol: Option<SymbolReference>, pub selected_symbol: Option<SymbolReference>,
pub reverse_fn_order: bool, pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool, pub disable_reverse_fn_order: bool,
pub show_hidden_symbols: bool,
} }
impl DiffViewState { impl DiffViewState {
@@ -62,19 +64,14 @@ impl DiffViewState {
self.symbol_state.disable_reverse_fn_order = false; self.symbol_state.disable_reverse_fn_order = false;
if let Ok(config) = config.read() { if let Ok(config) = config.read() {
if let Some(obj_path) = &config.obj_path { if let Some(obj_config) = &config.selected_obj {
if let Some(object) = config.objects.iter().find(|object| { if let Some(value) = obj_config.reverse_fn_order {
let path_string = object.path.to_string_lossy().to_string();
&path_string == obj_path
}) {
if let Some(value) = object.reverse_fn_order {
self.symbol_state.reverse_fn_order = value; self.symbol_state.reverse_fn_order = value;
self.symbol_state.disable_reverse_fn_order = true; self.symbol_state.disable_reverse_fn_order = true;
} }
} }
} }
} }
}
pub fn post_update(&mut self, _jobs: &mut JobQueue, config: &AppConfigRef) { pub fn post_update(&mut self, _jobs: &mut JobQueue, config: &AppConfigRef) {
if self.queue_build { if self.queue_build {
@@ -140,6 +137,9 @@ fn symbol_ui(
state: &mut SymbolViewState, state: &mut SymbolViewState,
appearance: &Appearance, appearance: &Appearance,
) -> Option<View> { ) -> Option<View> {
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols {
return None;
}
let mut ret = None; let mut ret = None;
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
let name: &str = let name: &str =
@@ -159,6 +159,9 @@ fn symbol_ui(
if symbol.flags.0.contains(ObjSymbolFlags::Weak) { if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone()); write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
} }
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
write_text("h", appearance.deemphasized_text_color, &mut job, appearance.code_font.clone());
}
write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone()); write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
if let Some(match_percent) = symbol.match_percent { if let Some(match_percent) = symbol.match_percent {
write_text("(", appearance.text_color, &mut job, appearance.code_font.clone()); write_text("(", appearance.text_color, &mut job, appearance.code_font.clone());
@@ -268,6 +271,15 @@ fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
}); });
} }
fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(appearance.replace_color, "No object configured");
});
}
pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) { pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) {
let DiffViewState { build, current_view, symbol_state, search, .. } = state; let DiffViewState { build, current_view, symbol_state, search, .. } = state;
let Some(result) = build else { let Some(result) = build else {
@@ -294,9 +306,13 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
ui.label("Build target:"); ui.label("Build target:");
if result.first_status.success { if result.first_status.success {
ui.label("OK"); if result.first_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else { } else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail"); ui.label("OK");
}
} else {
ui.colored_label(appearance.delete_color, "Fail");
} }
}); });
@@ -317,19 +333,19 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
ui.label("Build base:"); ui.label("Build base:");
if result.second_status.success { if result.second_status.success {
ui.label("OK"); if result.second_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else { } else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail"); ui.label("OK");
}
} else {
ui.colored_label(appearance.delete_color, "Fail");
} }
}); });
ui.add_enabled( if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
!symbol_state.disable_reverse_fn_order, state.queue_build = true;
egui::Checkbox::new( }
&mut symbol_state.reverse_fn_order,
"Reverse function order (-inline deferred)",
),
);
}, },
); );
}, },
@@ -353,6 +369,8 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
&lower_search, &lower_search,
appearance, appearance,
)); ));
} else {
missing_obj_ui(ui, appearance);
} }
} else { } else {
build_log_ui(ui, &result.first_status, appearance); build_log_ui(ui, &result.first_status, appearance);
@@ -370,6 +388,8 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
&lower_search, &lower_search,
appearance, appearance,
)); ));
} else {
missing_obj_ui(ui, appearance);
} }
} else { } else {
build_log_ui(ui, &result.second_status, appearance); build_log_ui(ui, &result.second_status, appearance);