Compare commits

..

18 Commits

Author SHA1 Message Date
Luke Street 0899e6779c Add alt shortcut support to many elements 2024-12-01 22:36:15 -07:00
LagoLunatic 95528fa8d2 Found a better place to clear the autoscroll flag
DiffViewState::post_update is where the flag gets set, so clearing it right before that at the start of the function seems to make the most sense, instead of doing it in App::update.
2024-12-01 22:36:15 -07:00
LagoLunatic 517b84e766 Simplify clearing of the autoscroll flag, remove &mut State 2024-12-01 22:36:15 -07:00
LagoLunatic 45dd73f5a9 Fix auto-scrolling to highlighted symbol only working for the left side
The flag is cleared after one scroll to avoid doing it continuously, but this breaks when we need to scroll to both the left and the right symbol at the same time. So now each side has its own flag to keep track of this state independently.
2024-12-01 22:36:15 -07:00
LagoLunatic d7d7a7f14a Add escape as an alternative to back hotkey 2024-12-01 22:36:15 -07:00
LagoLunatic 441b30070e Split function diff view: Enable PageUp/PageDown/Home/End for scrolling 2024-12-01 22:36:15 -07:00
LagoLunatic 28bd7182d1 Add hotkeys to change target and base functions 2024-12-01 22:36:13 -07:00
LagoLunatic 3f03a75825 Add space as alternative to enter hotkey
This is for consistency with egui's builtint enter/space hotkey for interacting with the focused widget.
2024-12-01 22:36:05 -07:00
LagoLunatic 4fb64a3ad4 Add Ctrl+F/S shortcuts for focusing the object and symbol filter text edits 2024-12-01 22:36:05 -07:00
LagoLunatic 77c104c67b Fix some hotkeys stealing input from focused widgets
e.g. The symbol list was stealing the W/S key presses when typing into the symbol filter text edit.

If the user actually wants to use these shortcuts while a widget is focused, they can simply press the escape key to unfocus all widgets and then press the shortcut.
2024-12-01 22:36:05 -07:00
LagoLunatic 046f3d6999 Auto-scroll the keyboard-selected symbols into view if offscreen 2024-12-01 22:36:05 -07:00
LagoLunatic 2427b06584 Do not clear highlighted symbol when hovering mouse over an unpaired symbol 2024-12-01 22:36:05 -07:00
LagoLunatic 157e99de6f Do not clear highlighted symbol when backing out of diff view 2024-12-01 22:36:05 -07:00
LagoLunatic b571787732 Add hotkeys to select the next symbol above/below the current one in the listing 2024-12-01 22:36:05 -07:00
LagoLunatic dbf86ec3cf Add scroll hotkeys 2024-12-01 22:36:05 -07:00
LagoLunatic acc1189150 Add enter and back hotkeys 2024-12-01 22:36:05 -07:00
LagoLunatic 123253c322 Update .gitignore 2024-12-01 22:36:05 -07:00
LagoLunatic ef680a5e7d Fix missing dependency feature for objdiff-gui 2024-12-01 22:36:05 -07:00
189 changed files with 13396 additions and 72335 deletions
+5 -4
View File
@@ -1,4 +1,5 @@
# statically link the C runtime so the executable does not depend on [target.x86_64-pc-windows-msvc]
# that shared/dynamic library. linker = "rust-lld"
[target.'cfg(all(target_env = "msvc", target_os = "windows"))']
rustflags = ["-C", "target-feature=+crt-static"] [target.aarch64-pc-windows-msvc]
linker = "rust-lld"
-2
View File
@@ -1,2 +0,0 @@
[*.md]
trim_trailing_whitespace = false
+36 -141
View File
@@ -4,16 +4,13 @@ on:
pull_request: pull_request:
push: push:
paths-ignore: paths-ignore:
- "*.md" - '*.md'
- "LICENSE*" - 'LICENSE*'
workflow_dispatch: workflow_dispatch:
permissions:
# For npm publish provenance
id-token: write
env: env:
BUILD_PROFILE: release-lto BUILD_PROFILE: release-lto
CARGO_TARGET_DIR: target
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
jobs: jobs:
@@ -28,7 +25,7 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get -y install libgtk-3-dev sudo apt-get -y install libgtk-3-dev
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
@@ -36,9 +33,9 @@ jobs:
- name: Cache Rust workspace - name: Cache Rust workspace
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Cargo check - name: Cargo check
run: cargo check --all-targets --all-features --workspace run: cargo check --all-features --all-targets
- name: Cargo clippy - name: Cargo clippy
run: cargo clippy --all-targets --all-features --workspace run: cargo clippy --all-features --all-targets
fmt: fmt:
name: Format name: Format
@@ -47,7 +44,7 @@ jobs:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Setup Rust toolchain
# We use nightly options in rustfmt.toml # We use nightly options in rustfmt.toml
uses: dtolnay/rust-toolchain@nightly uses: dtolnay/rust-toolchain@nightly
@@ -62,18 +59,19 @@ jobs:
strategy: strategy:
matrix: matrix:
checks: checks:
# - advisories - advisories
- bans licenses sources - bans licenses sources
# Prevent new advisories from failing CI # Prevent new advisories from failing CI
continue-on-error: ${{ matrix.checks == 'advisories' }} continue-on-error: ${{ matrix.checks == 'advisories' }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2 - uses: EmbarkStudios/cargo-deny-action@v1
with: with:
command: check ${{ matrix.checks }} command: check ${{ matrix.checks }}
test: test:
name: Test name: Test
if: 'false' # No tests yet
strategy: strategy:
matrix: matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ] platform: [ ubuntu-latest, windows-latest, macos-latest ]
@@ -86,13 +84,13 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get -y install libgtk-3-dev sudo apt-get -y install libgtk-3-dev
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache Rust workspace - name: Cache Rust workspace
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Cargo test - name: Cargo test
run: cargo test --release --all-features --workspace run: cargo test --release --all-features
build-cli: build-cli:
name: Build objdiff-cli name: Build objdiff-cli
@@ -145,15 +143,14 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install uv
if: matrix.build == 'zigbuild'
uses: astral-sh/setup-uv@v7
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
if: matrix.build == 'zigbuild' if: matrix.build == 'zigbuild'
run: | run: |
uv tool install cargo-zigbuild==0.20.1 --with-executables-from ziglang==0.15.1 python3 -m venv .venv
echo "CARGO_ZIGBUILD_ZIG_PATH=$(uv tool dir)/cargo-zigbuild/bin/python-zig" >> $GITHUB_ENV . .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
@@ -164,15 +161,15 @@ jobs:
key: ${{ matrix.target }} key: ${{ matrix.target }}
- name: Cargo build - name: Cargo build
run: > run: >
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }} --bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }} name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: | path: |
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }} ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error if-no-files-found: error
build-gui: build-gui:
@@ -183,26 +180,21 @@ jobs:
matrix: matrix:
include: include:
- platform: ubuntu-latest - platform: ubuntu-latest
target: x86_64-unknown-linux-gnu.2.31 target: x86_64-unknown-linux-gnu
target_base: x86_64-unknown-linux-gnu
name: linux-x86_64 name: linux-x86_64
packages: libgtk-3-dev packages: libgtk-3-dev
build: zigbuild
features: default features: default
- platform: windows-latest - platform: windows-latest
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
name: windows-x86_64 name: windows-x86_64
build: build
features: default features: default
- platform: macos-latest - platform: macos-latest
target: x86_64-apple-darwin target: x86_64-apple-darwin
name: macos-x86_64 name: macos-x86_64
build: build
features: default features: default
- platform: macos-latest - platform: macos-latest
target: aarch64-apple-darwin target: aarch64-apple-darwin
name: macos-arm64 name: macos-arm64
build: build
features: default features: default
fail-fast: false fail-fast: false
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
@@ -213,72 +205,39 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get -y install ${{ matrix.packages }} sudo apt-get -y install ${{ matrix.packages }}
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install uv
if: matrix.build == 'zigbuild'
uses: astral-sh/setup-uv@v7
- name: Install cargo-zigbuild
if: matrix.build == 'zigbuild'
run: |
uv tool install cargo-zigbuild==0.20.1 --with-executables-from ziglang==0.15.1
echo "CARGO_ZIGBUILD_ZIG_PATH=$(uv tool dir)/cargo-zigbuild/bin/python-zig" >> $GITHUB_ENV
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target_base || matrix.target }} targets: ${{ matrix.target }}
- name: Cache Rust workspace - name: Cache Rust workspace
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
key: ${{ matrix.target }} key: ${{ matrix.target }}
- name: Cargo build - name: Cargo build
run: > run: >
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }} --bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }} name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: | path: |
target/${{ matrix.target_base || matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }} ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
target/${{ matrix.target_base || matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error if-no-files-found: error
build-wasm: release:
name: Build objdiff-wasm name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Setup Node
uses: actions/setup-node@v6.4.0
with:
node-version: lts/*
- name: Install dependencies
run: npm -C objdiff-wasm ci
- name: Build
run: npm -C objdiff-wasm run build
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: wasm
path: objdiff-wasm/dist/
if-no-files-found: error
check-version:
name: Check package versions
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ build-cli, build-gui, build-wasm ] needs: [ build-cli, build-gui ]
permissions:
contents: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Check git tag against package versions - name: Check git tag against Cargo version
shell: bash shell: bash
run: | run: |
set -eou pipefail set -eou pipefail
@@ -290,24 +249,9 @@ jobs:
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)" echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
exit 1 exit 1
fi fi
version="v$(jq -r .version objdiff-wasm/package.json)"
if [ "$tag" != "$version" ]; then
echo "::error::Git tag doesn't match the npm version! ($tag != $version)"
exit 1
fi
release-github:
name: Release (GitHub)
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [ check-version ]
permissions:
contents: write
steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
pattern: objdiff-*
path: artifacts path: artifacts
- name: Rename artifacts - name: Rename artifacts
working-directory: artifacts working-directory: artifacts
@@ -337,52 +281,3 @@ jobs:
files: out/* files: out/*
draft: true draft: true
generate_release_notes: true generate_release_notes: true
release-cargo:
name: Release (Cargo)
runs-on: ubuntu-latest
needs: [ check-version ]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish -p objdiff-core
release-npm:
name: Release (npm)
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [ check-version ]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6.4.0
with:
node-version: lts/*
registry-url: 'https://registry.npmjs.org'
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: wasm
path: objdiff-wasm/dist
- name: Publish
working-directory: objdiff-wasm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
version=$(jq -r '.version' package.json)
tag="latest"
# Check for prerelease by looking for a dash
case "$version" in
*-*)
tag=$(echo "$version" | sed -e 's/^[^-]*-//' -e 's/\..*$//')
;;
esac
echo "Publishing version $version with tag '$tag'..."
npm publish --provenance --access public --tag "$tag"
-36
View File
@@ -1,36 +0,0 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: fix-byte-order-marker
- id: check-yaml
- id: check-added-large-files
- repo: local
hooks:
- id: cargo-fmt
name: cargo fmt
description: Run cargo fmt on all project files.
language: system
entry: cargo
args: ["+nightly", "fmt", "--all"]
pass_filenames: false
- id: cargo clippy
name: cargo clippy
description: Run cargo clippy on all project files.
language: system
entry: cargo
args: ["+nightly", "clippy", "--all-targets", "--all-features", "--workspace"]
pass_filenames: false
- id: cargo-deny
name: cargo deny
description: Run cargo deny on all project files.
language: system
entry: cargo
args: ["deny", "check"]
pass_filenames: false
always_run: true
-44
View File
@@ -1,44 +0,0 @@
# Repository Guidelines
## Project Structure & Module Organization
- `objdiff-core`: core library with diffing logic and shared components
- `objdiff-cli`: CLI/TUI with `ratatui`
- `objdiff-gui`: GUI with `eframe`/`egui`
- `objdiff-wasm`: WebAssembly bindings
objdiff has three main frontends: GUI, CLI/TUI, and [web](https://github.com/encounter/objdiff-web) (utilizing `objdiff-wasm`).
`objdiff-gui` is the most fully-featured and is the primary target for new features.
`objdiff-cli` has an interactive TUI `diff` mode (powered by `ratatui`) and a non-interactive `report` mode to generate detailed progress reports (e.g. for <https://decomp.dev>).
`objdiff-wasm` is compiled into a [WebAssembly Component](https://component-model.bytecodealliance.org/). The [API](objdiff-wasm/wit/objdiff.wit) is defined using [WIT](https://component-model.bytecodealliance.org/design/wit.html). The web interface is separate, but mirrors the GUI in functionality.
## Build, Test, and Development Commands
Run `cargo build` for a debug build and `cargo build --release` for an optimized build.
The CLI can be exercised with `cargo run --release --bin objdiff-cli -- --help`.
The WASM build is not included in the workspace by default due to differences in toolchain and target; build it with `npm -C objdiff-wasm run build`. (nightly required)
In general, do NOT use `--workspace`, as it will include the WASM crate and likely fail. Formatting and linting commands are exempt.
Pre-commit tasks:
- `cargo test` to run the test suite
- `cargo +nightly fmt --all` to format code (nightly required)
- `cargo +nightly clippy --all-targets --all-features --workspace -- -D warnings` to lint code (nightly required)
- `cargo deny check` (`cargo install --locked cargo-deny` if needed) if dependencies change
## Testing Guidelines
Favor focused unit tests adjacent to the code, and integration scenarios in `objdiff-core/tests`.
Integration tests utilize snapshots with the `insta` crate; run `curl -LsSf https://insta.rs/install.sh | sh` if needed.
To generate updated snapshots, use `cargo insta test`, review diffs manually, then accept changes with `cargo insta accept` (or simply `mv` to accept specific snapshots).
## Configuration Tips
- `config.schema.json`: JSON schema for the user-facing project config file (`objdiff.json`)
- `objdiff-core/config-schema.json`: Internal options schema to generate `DiffObjConfig` (see `objdiff-core/config_gen.rs`)
The project configuration (`objdiff.json`) is intended to be generated by the projects' build system. It includes paths to the target (expected) and base (current) object files, along with build commands and file watch patterns. See `README.md` for a summary of key options.
Generated
+1630 -3568
View File
File diff suppressed because it is too large Load Diff
+8 -19
View File
@@ -3,23 +3,8 @@ members = [
"objdiff-cli", "objdiff-cli",
"objdiff-core", "objdiff-core",
"objdiff-gui", "objdiff-gui",
"objdiff-wasm",
] ]
default-members = [ resolver = "2"
"objdiff-cli",
"objdiff-core",
"objdiff-gui",
# Exclude objdiff-wasm by default
]
resolver = "3"
[workspace.package]
version = "3.7.3"
authors = ["Luke Street <luke@street.dev>"]
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.88"
[profile.release-lto] [profile.release-lto]
inherits = "release" inherits = "release"
@@ -27,6 +12,10 @@ lto = "fat"
strip = "debuginfo" strip = "debuginfo"
codegen-units = 1 codegen-units = 1
[profile.release-min] [workspace.package]
inherits = "release-lto" version = "2.4.0"
opt-level = "z" authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.74"
+63 -82
View File
@@ -7,23 +7,20 @@ A local diffing tool for decompilation projects. Inspired by [decomp.me](https:/
Features: Features:
- Compare entire object files: functions and data - Compare entire object files: functions and data.
- Built-in C++ symbol demangling (GCC, MSVC, CodeWarrior, Itanium) - Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
- Automatic rebuild on source file changes - Automatic rebuild on source file changes.
- Project integration via [configuration file](#configuration) - Project integration via [configuration file](#configuration).
- Search and filter objects with quick switching - Search and filter all of a project's objects and quickly switch.
- Click-to-highlight values and registers - Click to highlight all instances of values and registers.
- Detailed progress reporting (powers [decomp.dev](https://decomp.dev))
- WebAssembly API, [web interface](https://github.com/encounter/objdiff-web) and [Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=decomp-dev.objdiff) (WIP)
Supports: Supports:
- ARM (GBA, DS, 3DS) - PowerPC 750CL (GameCube, Wii)
- ARM64 (Switch)
- MIPS (N64, PS1, PS2, PSP) - MIPS (N64, PS1, PS2, PSP)
- PowerPC (GameCube, Wii, PS3, Xbox 360) - x86 (COFF only at the moment)
- SuperH (Saturn, Dreamcast) - ARM (GBA, DS, 3DS)
- x86, x86_64 (PC) - ARM64 (Switch, experimental)
See [Usage](#usage) for more information. See [Usage](#usage) for more information.
@@ -42,7 +39,7 @@ For Linux and macOS, run `chmod +x objdiff-*` to make the binary executable.
### CLI ### CLI
CLI binaries are available on the [releases page](https://github.com/encounter/objdiff/releases). CLI binaries can be found on the [releases page](https://github.com/encounter/objdiff/releases).
## Screenshots ## Screenshots
@@ -51,30 +48,33 @@ CLI binaries are available on the [releases page](https://github.com/encounter/o
## Usage ## Usage
objdiff compares two relocatable object files (`.o`). Here's how it works: 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.
1. **Create an `objdiff.json` configuration file** in your project root (or generate it with your build script). For example, if the target ("expected") object is located at `build/asm/MetroTRK/mslsupp.o` and the base ("actual")
This file lists **all objects in the project** with their target ("expected") and base ("current") paths. object is located at `build/src/MetroTRK/mslsupp.o`, the following configuration would be used:
2. **Load the project** in objdiff. - Target build directory: `build/asm`
- Base build directory: `build/src`
- Object: `MetroTRK/mslsupp.o`
3. **Select an object** from the sidebar to begin diffing. objdiff will then execute the build system from the project directory to build both objects:
4. **objdiff automatically:** ```sh
- Executes the build system to compile the base object (from current source code) $ make build/asm/MetroTRK/mslsupp.o # Only if "Build target object" is enabled
- Compares the two objects and displays the differences $ make build/src/MetroTRK/mslsupp.o
- Watches for source file changes and rebuilds when detected ```
The configuration file allows complete flexibility in project structure - your build directories can have any layout as long as the paths are specified correctly. The objects will then be compared and the results will be displayed in the UI.
See [Configuration](#configuration) for setup details. See [Configuration](#configuration) for more information.
## Configuration ## Configuration
Projects can add an `objdiff.json` file to configure the tool automatically. The configuration file must be located in While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` file to configure the tool automatically. The configuration file must be located in
the root project directory. the root project directory.
If your project has a generator script (e.g. `configure.py`), it's highly recommended to generate the objdiff configuration 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. file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it from being committed.
```json ```json
@@ -89,31 +89,22 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
"build_base": true, "build_base": true,
"watch_patterns": [ "watch_patterns": [
"*.c", "*.c",
"*.cc",
"*.cp", "*.cp",
"*.cpp", "*.cpp",
"*.cxx", "*.cxx",
"*.c++",
"*.h", "*.h",
"*.hh",
"*.hp", "*.hp",
"*.hpp", "*.hpp",
"*.hxx", "*.hxx",
"*.h++",
"*.pch",
"*.pch++",
"*.inc",
"*.s", "*.s",
"*.S", "*.S",
"*.asm", "*.asm",
"*.inc",
"*.py", "*.py",
"*.yml", "*.yml",
"*.txt", "*.txt",
"*.json" "*.json"
], ],
"ignore_patterns": [
"build/**/*"
],
"units": [ "units": [
{ {
"name": "main/MetroTRK/mslsupp", "name": "main/MetroTRK/mslsupp",
@@ -127,70 +118,60 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
### Schema ### Schema
> [!NOTE] View [config.schema.json](config.schema.json) for all available options. The below list is a summary of the most important options.
> View [config.schema.json](config.schema.json) for all available options. Below is a summary of the most important options.
#### Build Configuration `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.
The build command will be `[custom_make] [custom_args] path/to/object.o`.
**`custom_make`** _(optional, default: `"make"`)_ `custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
If the project uses a different build system (e.g. `ninja`), specify it here. The build command will be `[custom_make] [custom_args] path/to/object.o`.
**`custom_args`** _(optional)_ `build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g.
Additional arguments to pass to the build command prior to the object path. `make path/to/target.o`).
This is useful if the target objects are not built by default or can change based on project configuration or edits
to assembly files.
Requires the build system to be configured properly.
**`build_target`** _(default: `false`)_ `build_base`: If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).
If true, objdiff will build the target objects before diffing (e.g. `make path/to/target.o`). Useful if target objects are not built by default or can change based on project configuration. Requires proper build system configuration. It's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.
**`build_base`** _(default: `true`)_ `watch_patterns` _(optional)_: A list of glob patterns to watch for changes.
If true, objdiff will build the base objects before diffing (e.g. `make path/to/base.o`). It's unlikely you'll want to disable this unless using an external tool to rebuild the base object. ([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.
#### File Watching `units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
**`watch_patterns`** _(optional, default: listed above)_ > `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
A list of glob patterns to watch for changes ([supported syntax](https://docs.rs/globset/latest/globset/#syntax)). When these files change, objdiff automatically rebuilds and re-compares objects. >
> `target_path`: Path to the "target" or "expected" object from the project root.
**`ignore_patterns`** _(optional, default: listed above)_ > This object is the **intended result** of the match.
A list of glob patterns to explicitly ignore when watching for changes ([supported syntax](https://docs.rs/globset/latest/globset/#syntax)). >
> `base_path`: Path to the "base" or "actual" object from the project root.
#### Units (Objects) > This object is built from the **current source code**.
>
**`units`** _(optional)_ > `metadata.auto_generated` _(optional)_: Hides the object from the object list, but still includes it in reports.
If specified, objdiff displays a list of objects in the sidebar for easy navigation. Each unit contains: >
> `metadata.complete` _(optional)_: Marks the object as "complete" (or "linked") in the object list.
- **`name`** _(optional)_ - The display name in the UI. Defaults to the object's `path`. > This is useful for marking objects that are fully decompiled. A value of `false` will mark the object as "incomplete".
- **`target_path`** _(optional)_ - Path to the "target" or "expected" object (the **intended result**).
- **`base_path`** _(optional)_ - Path to the "base" or "current" object (built from **current source code**). Omit if there is no source object yet.
- **`metadata.auto_generated`** _(optional)_ - Hides the object from the sidebar but includes it in progress reports.
- **`metadata.complete`** _(optional)_ - Marks the object as "complete" (linked) when `true` or "incomplete" when `false`.
## Building ## Building
Install Rust via [rustup](https://rustup.rs). Install Rust via [rustup](https://rustup.rs).
```shell ```shell
git clone https://github.com/encounter/objdiff.git $ git clone https://github.com/encounter/objdiff.git
cd objdiff $ cd objdiff
cargo run --release $ cargo run --release
``` ```
Or install directly with cargo: Or using `cargo install`.
```shell ```shell
cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli $ cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
``` ```
Binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`. The binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
## Contributing
Install `pre-commit` to run linting and formatting automatically:
```shell
rustup toolchain install nightly # Required for cargo fmt/clippy
cargo install --locked cargo-deny # https://github.com/EmbarkStudios/cargo-deny
uv tool install pre-commit # https://docs.astral.sh/uv, or use pipx or pip
pre-commit install
```
## License ## License
+10 -63
View File
@@ -15,7 +15,7 @@
}, },
"custom_make": { "custom_make": {
"type": "string", "type": "string",
"description": "If the project uses a different build system (e.g. ninja), specify it here.\nThe build command will be `[custom_make] [custom_args] path/to/object.o`.", "description": "By default, objdiff will use make to build the project.\nIf the project uses a different build system (e.g. ninja), specify it here.\nThe build command will be `[custom_make] [custom_args] path/to/object.o`.",
"examples": [ "examples": [
"make", "make",
"ninja" "ninja"
@@ -41,55 +41,39 @@
}, },
"build_target": { "build_target": {
"type": "boolean", "type": "boolean",
"description": "If true, objdiff will build the target objects before diffing (e.g. `make path/to/target.o`).\nUseful if target objects are not built by default or can change based on project configuration.\nRequires proper build system configuration.", "description": "If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`).\nThis is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files.\nRequires the build system to be configured properly.",
"default": false "default": false
}, },
"build_base": { "build_base": {
"type": "boolean", "type": "boolean",
"description": "If true, objdiff will build the base objects before diffing (e.g. `make path/to/base.o`).\nIt's unlikely you'll want to disable this unless using an external tool to rebuild the base object.", "description": "If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).\nIt's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.",
"default": true "default": true
}, },
"watch_patterns": { "watch_patterns": {
"type": "array", "type": "array",
"description": "A list of glob patterns to watch for changes.\nWhen these files change, objdiff automatically rebuilds and re-compares objects.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax", "description": "List of glob patterns to watch for changes in the project.\nIf any of these files change, objdiff will automatically rebuild the objects and re-compare them.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
"items": { "items": {
"type": "string" "type": "string"
}, },
"default": [ "default": [
"*.c", "*.c",
"*.cc",
"*.cp", "*.cp",
"*.cpp", "*.cpp",
"*.cxx", "*.cxx",
"*.c++",
"*.h", "*.h",
"*.hh",
"*.hp", "*.hp",
"*.hpp", "*.hpp",
"*.hxx", "*.hxx",
"*.h++",
"*.pch",
"*.pch++",
"*.inc",
"*.s", "*.s",
"*.S", "*.S",
"*.asm", "*.asm",
"*.inc",
"*.py", "*.py",
"*.yml", "*.yml",
"*.txt", "*.txt",
"*.json" "*.json"
] ]
}, },
"ignore_patterns": {
"type": "array",
"description": "A list of glob patterns to explicitly ignore when watching for changes.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
"items": {
"type": "string"
},
"default": [
"build/**/*"
]
},
"objects": { "objects": {
"type": "array", "type": "array",
"description": "Use units instead.", "description": "Use units instead.",
@@ -111,25 +95,6 @@
"items": { "items": {
"$ref": "#/$defs/progress_category" "$ref": "#/$defs/progress_category"
} }
},
"options": {
"type": "object",
"description": "Diff configuration options that should be applied automatically when the project is loaded.",
"additionalProperties": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"examples": [
{
"demangler": "gnu_legacy"
}
]
} }
}, },
"$defs": { "$defs": {
@@ -138,7 +103,7 @@
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"description": "The display name in the UI. Defaults to the object's path." "description": "The name of the object in the UI. If not specified, the object's path will be used."
}, },
"path": { "path": {
"type": "string", "type": "string",
@@ -147,11 +112,11 @@
}, },
"target_path": { "target_path": {
"type": "string", "type": "string",
"description": "Path to the \"target\" or \"expected\" object (the intended result)." "description": "Path to the target object from the project root.\nRequired if path is not specified."
}, },
"base_path": { "base_path": {
"type": "string", "type": "string",
"description": "Path to the \"base\" or \"current\" object (built from current source code).\nOmit if there is no source object yet." "description": "Path to the base object from the project root.\nRequired if path is not specified."
}, },
"reverse_fn_order": { "reverse_fn_order": {
"type": "boolean", "type": "boolean",
@@ -175,20 +140,6 @@
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} }
},
"options": {
"type": "object",
"description": "Diff configuration options that should be applied when this unit is active.",
"additionalProperties": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
}
} }
} }
}, },
@@ -224,10 +175,6 @@
"type": "boolean", "type": "boolean",
"description": "If true, objdiff will run the build command with the context file as an argument to generate it.", "description": "If true, objdiff will run the build command with the context file as an argument to generate it.",
"default": false "default": false
},
"preset_id": {
"type": "number",
"description": "The decomp.me preset ID to use for the scratch.\nCompiler and flags in the config will take precedence over the preset, but the preset is useful for organizational purposes."
} }
}, },
"required": [ "required": [
@@ -240,7 +187,7 @@
"properties": { "properties": {
"complete": { "complete": {
"type": "boolean", "type": "boolean",
"description": "Marks the object as \"complete\" (linked) when `true` or \"incomplete\" when `false`." "description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\"."
}, },
"reverse_fn_order": { "reverse_fn_order": {
"type": "boolean", "type": "boolean",
@@ -260,7 +207,7 @@
}, },
"auto_generated": { "auto_generated": {
"type": "boolean", "type": "boolean",
"description": "Hides the object from the sidebar but includes it in progress reports." "description": "Hides the object from the object list by default, but still includes it in reports."
} }
} }
}, },
+4 -13
View File
@@ -73,15 +73,7 @@ ignore = [
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate is an indirect dependency" }, { id = "RUSTSEC-2024-0384", reason = "Unmaintained indirect dependency" },
{ id = "RUSTSEC-2025-0052", reason = "Unmaintained async-std crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0134", reason = "Unmaintained rustls-pemfile crate is an indirect dependency" },
{ id = "RUSTSEC-2026-0098", reason = "Build-time objdiff-wasm wit-deps dependency still uses reqwest 0.11; latest wit-deps has no safe upgrade path" },
{ id = "RUSTSEC-2026-0099", reason = "Build-time objdiff-wasm wit-deps dependency still uses reqwest 0.11; latest wit-deps has no safe upgrade path" },
{ id = "RUSTSEC-2026-0104", reason = "Build-time objdiff-wasm wit-deps dependency still uses reqwest 0.11; latest wit-deps has no safe upgrade path" },
{ id = "RUSTSEC-2026-0192", reason = "Unmaintained ttf-parser is an egui 0.33 indirect dependency; egui 0.35 requires raising MSRV from 1.88 to 1.92" },
{ id = "RUSTSEC-2026-0194", reason = "Build-time wayland-scanner dependency parses vendored protocol XML; no newer stable wayland-scanner release is available" },
{ id = "RUSTSEC-2026-0195", reason = "Build-time wayland-scanner dependency parses vendored protocol XML; no newer stable wayland-scanner release is available" },
] ]
# If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library. # If this is false, then it uses a built-in git library.
@@ -110,8 +102,8 @@ allow = [
"Zlib", "Zlib",
"0BSD", "0BSD",
"OFL-1.1", "OFL-1.1",
"Ubuntu-font-1.0", "LicenseRef-UFL-1.0",
"CDLA-Permissive-2.0", "OpenSSL",
] ]
# The confidence threshold for detecting a license from license text. # The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the # The higher the value, the more closely the license text must be to the
@@ -248,8 +240,7 @@ allow-git = []
[sources.allow-org] [sources.allow-org]
# github.com organizations to allow git sources for # github.com organizations to allow git sources for
github = [ github = ["notify-rs"]
]
# gitlab.com organizations to allow git sources for # gitlab.com organizations to allow git sources for
gitlab = [] gitlab = []
# bitbucket.org organizations to allow git sources for # bitbucket.org organizations to allow git sources for
+6 -7
View File
@@ -14,21 +14,20 @@ publish = false
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
argp = "0.4" argp = "0.3"
crossterm = "0.29" crossterm = "0.28"
enable-ansi-support = "0.3" enable-ansi-support = "0.2"
memmap2 = "0.9" memmap2 = "0.9"
objdiff-core = { path = "../objdiff-core", features = ["all"] } objdiff-core = { path = "../objdiff-core", features = ["all"] }
prost = "0.14" prost = "0.13"
ratatui = "0.30" ratatui = "0.29"
rayon = "1.11" rayon = "1.10"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
supports-color = "3.0" supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] } time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
typed-path = "0.12"
[target.'cfg(target_env = "musl")'.dependencies] [target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1" mimalloc = "0.1"
+1 -1
View File
@@ -4,7 +4,7 @@
//! For now, this only adds a --version/-V option which causes early-exit. //! For now, this only adds a --version/-V option which causes early-exit.
use std::ffi::OsStr; use std::ffi::OsStr;
use argp::{EarlyExit, FromArgs, TopLevelCommand, parser::ParseGlobalOptions}; use argp::{parser::ParseGlobalOptions, EarlyExit, FromArgs, TopLevelCommand};
struct ArgsOrVersion<T>(T) struct ArgsOrVersion<T>(T)
where T: FromArgs; where T: FromArgs;
File diff suppressed because it is too large Load Diff
-31
View File
@@ -1,33 +1,2 @@
pub mod diff; pub mod diff;
pub mod report; pub mod report;
use std::str::FromStr;
use anyhow::{Context, Result, anyhow};
use objdiff_core::diff::{ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig};
pub fn apply_config_args(diff_config: &mut DiffObjConfig, args: &[String]) -> Result<()> {
for config in args {
let (key, value) = config.split_once('=').context("--config expects \"key=value\"")?;
let property_id = ConfigPropertyId::from_str(key)
.map_err(|()| anyhow!("Invalid configuration property: {}", key))?;
diff_config.set_property_value_str(property_id, value).map_err(|()| {
let mut options = String::new();
match property_id.kind() {
ConfigPropertyKind::Boolean => {
options = "true, false".to_string();
}
ConfigPropertyKind::Choice(variants) => {
for (i, variant) in variants.iter().enumerate() {
if i > 0 {
options.push_str(", ");
}
options.push_str(variant.value);
}
}
}
anyhow!("Invalid value for {}. Expected one of: {}", property_id.name(), options)
})?;
}
Ok(())
}
+102 -158
View File
@@ -1,25 +1,28 @@
use std::{collections::HashSet, fs::File, io::Read, time::Instant}; use std::{
collections::HashSet,
fs::File,
io::Read,
path::{Path, PathBuf},
time::Instant,
};
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use argp::FromArgs; use argp::FromArgs;
use objdiff_core::{ use objdiff_core::{
bindings::report::{ bindings::report::{
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, REPORT_VERSION, ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report,
Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
REPORT_VERSION,
}, },
config::{ProjectObject, ProjectOptions, apply_project_options, path::platform_path}, config::ProjectObject,
diff, diff, obj,
obj::{self, SectionKind, SymbolFlag, SymbolKind}, obj::{ObjSectionKind, ObjSymbolFlags},
}; };
use prost::Message; use prost::Message;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use tracing::{info, warn}; use tracing::{info, warn};
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
use crate::{ use crate::util::output::{write_output, OutputFormat};
cmd::{apply_config_args, diff::ObjectConfig},
util::output::{OutputFormat, write_output},
};
#[derive(FromArgs, PartialEq, Debug)] #[derive(FromArgs, PartialEq, Debug)]
/// Generate a progress report for a project. /// Generate a progress report for a project.
@@ -40,36 +43,33 @@ pub enum SubCommand {
/// Generate a progress report for a project. /// Generate a progress report for a project.
#[argp(subcommand, name = "generate")] #[argp(subcommand, name = "generate")]
pub struct GenerateArgs { pub struct GenerateArgs {
#[argp(option, short = 'p', from_str_fn(platform_path))] #[argp(option, short = 'p')]
/// Project directory /// Project directory
project: Option<Utf8PlatformPathBuf>, project: Option<PathBuf>,
#[argp(option, short = 'o', from_str_fn(platform_path))] #[argp(option, short = 'o')]
/// Output file /// Output file
output: Option<Utf8PlatformPathBuf>, output: Option<PathBuf>,
#[argp(switch, short = 'd')] #[argp(switch, short = 'd')]
/// Deduplicate global and weak symbols (runs single-threaded) /// Deduplicate global and weak symbols (runs single-threaded)
deduplicate: bool, deduplicate: bool,
#[argp(option, short = 'f')] #[argp(option, short = 'f')]
/// Output format (json, json-pretty, proto) (default: json) /// Output format (json, json-pretty, proto) (default: json)
format: Option<String>, format: Option<String>,
#[argp(option, short = 'c')]
/// Configuration property (key=value)
config: Vec<String>,
} }
#[derive(FromArgs, PartialEq, Debug)] #[derive(FromArgs, PartialEq, Debug)]
/// List any changes from a previous report. /// List any changes from a previous report.
#[argp(subcommand, name = "changes")] #[argp(subcommand, name = "changes")]
pub struct ChangesArgs { pub struct ChangesArgs {
#[argp(positional, from_str_fn(platform_path))] #[argp(positional)]
/// Previous report file /// Previous report file
previous: Utf8PlatformPathBuf, previous: PathBuf,
#[argp(positional, from_str_fn(platform_path))] #[argp(positional)]
/// Current report file /// Current report file
current: Utf8PlatformPathBuf, current: PathBuf,
#[argp(option, short = 'o', from_str_fn(platform_path))] #[argp(option, short = 'o')]
/// Output file /// Output file
output: Option<Utf8PlatformPathBuf>, output: Option<PathBuf>,
#[argp(option, short = 'f')] #[argp(option, short = 'f')]
/// Output format (json, json-pretty, proto) (default: json) /// Output format (json, json-pretty, proto) (default: json)
format: Option<String>, format: Option<String>,
@@ -83,46 +83,18 @@ pub fn run(args: Args) -> Result<()> {
} }
fn generate(args: GenerateArgs) -> Result<()> { fn generate(args: GenerateArgs) -> Result<()> {
let base_diff_config = diff::DiffObjConfig {
function_reloc_diffs: diff::FunctionRelocDiffs::None,
combine_data_sections: true,
combine_text_sections: true,
ppc_calculate_pool_relocations: false,
..Default::default()
};
let output_format = OutputFormat::from_option(args.format.as_deref())?; let output_format = OutputFormat::from_option(args.format.as_deref())?;
let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new(".")); let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
info!("Loading project {}", project_dir); info!("Loading project {}", project_dir.display());
let project = match objdiff_core::config::try_project_config(project_dir.as_ref()) { let mut project = match objdiff_core::config::try_project_config(project_dir) {
Some((Ok(config), _)) => config, Some((Ok(config), _)) => config,
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err), Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
None => bail!("No project configuration found"), None => bail!("No project configuration found"),
}; };
let target_obj_dir =
project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let base_obj_dir =
project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let project_units = project.units.as_deref().unwrap_or_default();
let objects = project_units
.iter()
.enumerate()
.map(|(idx, o)| {
(
ObjectConfig::new(
o,
project_dir,
target_obj_dir.as_deref(),
base_obj_dir.as_deref(),
),
idx,
)
})
.collect::<Vec<_>>();
info!( info!(
"Generating report for {} units (using {} threads)", "Generating report for {} units (using {} threads)",
objects.len(), project.units().len(),
if args.deduplicate { 1 } else { rayon::current_num_threads() } if args.deduplicate { 1 } else { rayon::current_num_threads() }
); );
@@ -131,29 +103,31 @@ fn generate(args: GenerateArgs) -> Result<()> {
let mut existing_functions: HashSet<String> = HashSet::new(); let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate { if args.deduplicate {
// If deduplicating, we need to run single-threaded // If deduplicating, we need to run single-threaded
for (object, unit_idx) in &objects { for object in project.units.as_deref_mut().unwrap_or_default() {
let diff_config = build_unit_diff_config( if let Some(unit) = report_object(
&base_diff_config, object,
project.options.as_ref(), project_dir,
project_units.get(*unit_idx).and_then(ProjectObject::options), project.target_dir.as_deref(),
&args.config, project.base_dir.as_deref(),
)?; Some(&mut existing_functions),
if let Some(unit) = report_object(object, &diff_config, Some(&mut existing_functions))? )? {
{
units.push(unit); units.push(unit);
} }
} }
} else { } else {
let vec = objects let vec = project
.par_iter() .units
.map(|(object, unit_idx)| { .as_deref_mut()
let diff_config = build_unit_diff_config( .unwrap_or_default()
&base_diff_config, .par_iter_mut()
project.options.as_ref(), .map(|object| {
project_units.get(*unit_idx).and_then(ProjectObject::options), report_object(
&args.config, object,
)?; project_dir,
report_object(object, &diff_config, None) project.target_dir.as_deref(),
project.base_dir.as_deref(),
None,
)
}) })
.collect::<Result<Vec<Option<ReportUnit>>>>()?; .collect::<Result<Vec<Option<ReportUnit>>>>()?;
units = vec.into_iter().flatten().collect(); units = vec.into_iter().flatten().collect();
@@ -176,74 +150,56 @@ fn generate(args: GenerateArgs) -> Result<()> {
Ok(()) Ok(())
} }
fn build_unit_diff_config(
base: &diff::DiffObjConfig,
project_options: Option<&ProjectOptions>,
unit_options: Option<&ProjectOptions>,
cli_args: &[String],
) -> Result<diff::DiffObjConfig> {
let mut diff_config = base.clone();
if let Some(options) = project_options {
apply_project_options(&mut diff_config, options)?;
}
if let Some(options) = unit_options {
apply_project_options(&mut diff_config, options)?;
}
// CLI args override project and unit options
apply_config_args(&mut diff_config, cli_args)?;
Ok(diff_config)
}
fn report_object( fn report_object(
object: &ObjectConfig, object: &mut ProjectObject,
diff_config: &diff::DiffObjConfig, project_dir: &Path,
target_dir: Option<&Path>,
base_dir: Option<&Path>,
mut existing_functions: Option<&mut HashSet<String>>, mut existing_functions: Option<&mut HashSet<String>>,
) -> Result<Option<ReportUnit>> { ) -> Result<Option<ReportUnit>> {
object.resolve_paths(project_dir, target_dir, base_dir);
match (&object.target_path, &object.base_path) { match (&object.target_path, &object.base_path) {
(None, Some(_)) if !object.complete.unwrap_or(false) => { (None, Some(_)) if !object.complete().unwrap_or(false) => {
warn!("Skipping object without target: {}", object.name); warn!("Skipping object without target: {}", object.name());
return Ok(None); return Ok(None);
} }
(None, None) => { (None, None) => {
warn!("Skipping object without target or base: {}", object.name); warn!("Skipping object without target or base: {}", object.name());
return Ok(None); return Ok(None);
} }
_ => {} _ => {}
} }
let mapping_config = diff::MappingConfig { let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
mappings: object.symbol_mappings.clone(),
selecting_left: None,
selecting_right: None,
};
let target = object let target = object
.target_path .target_path
.as_ref() .as_ref()
.map(|p| { .map(|p| {
obj::read::read(p.as_ref(), diff_config, diff::DiffSide::Target) obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
.with_context(|| format!("Failed to open {p}"))
}) })
.transpose()?; .transpose()?;
let base = object let base = object
.base_path .base_path
.as_ref() .as_ref()
.map(|p| { .map(|p| {
obj::read::read(p.as_ref(), diff_config, diff::DiffSide::Base) obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
.with_context(|| format!("Failed to open {p}"))
}) })
.transpose()?; .transpose()?;
let result = let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
diff::diff_objs(target.as_ref(), base.as_ref(), None, diff_config, &mapping_config)?;
let metadata = ReportUnitMetadata { let metadata = ReportUnitMetadata {
complete: object.metadata.complete, complete: object.complete(),
module_name: target module_name: target
.as_ref() .as_ref()
.and_then(|o| o.split_meta.as_ref()) .and_then(|o| o.split_meta.as_ref())
.and_then(|m| m.module_name.clone()), .and_then(|m| m.module_name.clone()),
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id), module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
source_path: object.metadata.source_path.as_ref().map(|p| p.to_string()), source_path: object.metadata.as_ref().and_then(|m| m.source_path.clone()),
progress_categories: object.metadata.progress_categories.clone().unwrap_or_default(), progress_categories: object
auto_generated: object.metadata.auto_generated, .metadata
.as_ref()
.and_then(|m| m.progress_categories.clone())
.unwrap_or_default(),
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
}; };
let mut measures = Measures { total_units: 1, ..Default::default() }; let mut measures = Measures { total_units: 1, ..Default::default() };
let mut sections = vec![]; let mut sections = vec![];
@@ -251,16 +207,15 @@ fn report_object(
let obj = target.as_ref().or(base.as_ref()).unwrap(); let obj = target.as_ref().or(base.as_ref()).unwrap();
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap(); let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
for ((section_idx, section), section_diff) in for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
obj.sections.iter().enumerate().zip(&obj_diff.sections)
{
if section.kind == SectionKind::Unknown {
continue;
}
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| { let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object, // Support cases where we don't have a target object,
// assume complete means 100% match // assume complete means 100% match
if object.complete.unwrap_or(false) { 100.0 } else { 0.0 } if object.complete().unwrap_or(false) {
100.0
} else {
0.0
}
}); });
sections.push(ReportItem { sections.push(ReportItem {
name: section.name.clone(), name: section.name.clone(),
@@ -270,40 +225,39 @@ fn report_object(
demangled_name: None, demangled_name: None,
virtual_address: section.virtual_address, virtual_address: section.virtual_address,
}), }),
address: None,
}); });
match section.kind { match section.kind {
SectionKind::Data | SectionKind::Bss => { ObjSectionKind::Data | ObjSectionKind::Bss => {
measures.total_data += section.size; measures.total_data += section.size;
if section_match_percent == 100.0 { if section_match_percent == 100.0 {
measures.matched_data += section.size; measures.matched_data += section.size;
} }
continue; continue;
} }
_ => {} ObjSectionKind::Code => (),
} }
for (symbol, symbol_diff) in obj.symbols.iter().zip(&obj_diff.symbols) { for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if symbol.section != Some(section_idx) if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
|| symbol.size == 0
|| symbol.flags.contains(SymbolFlag::Hidden)
|| symbol.flags.contains(SymbolFlag::Ignored)
|| symbol.kind == SymbolKind::Section
{
continue; continue;
} }
if let Some(existing_functions) = &mut existing_functions if let Some(existing_functions) = &mut existing_functions {
&& (symbol.flags.contains(SymbolFlag::Global) if (symbol.flags.0.contains(ObjSymbolFlags::Global)
|| symbol.flags.contains(SymbolFlag::Weak)) || symbol.flags.0.contains(ObjSymbolFlags::Weak))
&& !existing_functions.insert(symbol.name.clone()) && !existing_functions.insert(symbol.name.clone())
{ {
continue; continue;
}
} }
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| { let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object, // Support cases where we don't have a target object,
// assume complete means 100% match // assume complete means 100% match
if object.complete.unwrap_or(false) { 100.0 } else { 0.0 } if object.complete().unwrap_or(false) {
100.0
} else {
0.0
}
}); });
measures.fuzzy_match_percent += match_percent * symbol.size as f32; measures.fuzzy_match_percent += match_percent * symbol.size as f32;
measures.total_code += symbol.size; measures.total_code += symbol.size;
@@ -318,7 +272,6 @@ fn report_object(
demangled_name: symbol.demangled_name.clone(), demangled_name: symbol.demangled_name.clone(),
virtual_address: symbol.virtual_address, virtual_address: symbol.virtual_address,
}), }),
address: symbol.address.checked_sub(section.address),
}); });
if match_percent == 100.0 { if match_percent == 100.0 {
measures.matched_functions += 1; measures.matched_functions += 1;
@@ -326,16 +279,6 @@ fn report_object(
measures.total_functions += 1; measures.total_functions += 1;
} }
} }
sections.sort_by(|a, b| a.name.cmp(&b.name));
let reverse_fn_order = object.metadata.reverse_fn_order.unwrap_or(false);
functions.sort_by(|a, b| {
if reverse_fn_order {
b.address.unwrap_or(0).cmp(&a.address.unwrap_or(0))
} else {
a.address.unwrap_or(u64::MAX).cmp(&b.address.unwrap_or(u64::MAX))
}
.then_with(|| a.size.cmp(&b.size))
});
if metadata.complete.unwrap_or(false) { if metadata.complete.unwrap_or(false) {
measures.complete_code = measures.total_code; measures.complete_code = measures.total_code;
measures.complete_data = measures.total_data; measures.complete_data = measures.total_data;
@@ -344,7 +287,7 @@ fn report_object(
measures.calc_fuzzy_match_percent(); measures.calc_fuzzy_match_percent();
measures.calc_matched_percent(); measures.calc_matched_percent();
Ok(Some(ReportUnit { Ok(Some(ReportUnit {
name: object.name.clone(), name: object.name().to_string(),
measures: Some(measures), measures: Some(measures),
sections, sections,
functions, functions,
@@ -354,7 +297,7 @@ fn report_object(
fn changes(args: ChangesArgs) -> Result<()> { fn changes(args: ChangesArgs) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?; let output_format = OutputFormat::from_option(args.format.as_deref())?;
let (previous, current) = if args.previous == "-" && args.current == "-" { let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
// Special case for comparing two reports from stdin // Special case for comparing two reports from stdin
let mut data = vec![]; let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?; std::io::stdin().read_to_end(&mut data)?;
@@ -469,14 +412,15 @@ fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
.collect() .collect()
} }
fn read_report(path: &Utf8PlatformPath) -> Result<Report> { fn read_report(path: &Path) -> Result<Report> {
if path == Utf8PlatformPath::new("-") { if path == Path::new("-") {
let mut data = vec![]; let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?; std::io::stdin().read_to_end(&mut data)?;
return Report::parse(&data).with_context(|| "Failed to load report from stdin"); return Report::parse(&data).with_context(|| "Failed to load report from stdin");
} }
let file = File::open(path).with_context(|| format!("Failed to open {path}"))?; let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
let mmap = let mmap = unsafe { memmap2::Mmap::map(&file) }
unsafe { memmap2::Mmap::map(&file) }.with_context(|| format!("Failed to map {path}"))?; .with_context(|| format!("Failed to map {}", path.display()))?;
Report::parse(mmap.as_ref()).with_context(|| format!("Failed to load report {path}")) Report::parse(mmap.as_ref())
.with_context(|| format!("Failed to load report {}", path.display()))
} }
+1 -4
View File
@@ -1,9 +1,6 @@
#![allow(clippy::too_many_arguments)]
mod argp_version; mod argp_version;
mod cmd; mod cmd;
mod util; mod util;
mod views;
// musl's allocator is very slow, so use mimalloc when targeting musl. // musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size. // Otherwise, use the system allocator to avoid extra code size.
@@ -17,7 +14,7 @@ use anyhow::{Error, Result};
use argp::{FromArgValue, FromArgs}; use argp::{FromArgValue, FromArgs};
use enable_ansi_support::enable_ansi_support; use enable_ansi_support::enable_ansi_support;
use supports_color::Stream; use supports_color::Stream;
use tracing_subscriber::{EnvFilter, filter::LevelFilter}; use tracing_subscriber::{filter::LevelFilter, EnvFilter};
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
enum LogLevel { enum LogLevel {
+4 -7
View File
@@ -5,7 +5,7 @@ use std::{
path::Path, path::Path,
}; };
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use tracing::info; use tracing::info;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
@@ -34,12 +34,9 @@ impl OutputFormat {
} }
} }
pub fn write_output<T, P>(input: &T, output: Option<P>, format: OutputFormat) -> Result<()> pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
where where T: serde::Serialize + prost::Message {
T: serde::Serialize + prost::Message, match output {
P: AsRef<Path>,
{
match output.as_ref().map(|p| p.as_ref()) {
Some(output) if output != Path::new("-") => { Some(output) if output != Path::new("-") => {
info!("Writing to {}", output.display()); info!("Writing to {}", output.display());
let file = File::options() let file = File::options()
+1 -1
View File
@@ -3,7 +3,7 @@ use std::{io::stdout, panic};
use crossterm::{ use crossterm::{
cursor::Show, cursor::Show,
event::DisableMouseCapture, event::DisableMouseCapture,
terminal::{LeaveAlternateScreen, disable_raw_mode}, terminal::{disable_raw_mode, LeaveAlternateScreen},
}; };
pub fn crossterm_panic_handler() { pub fn crossterm_panic_handler() {
File diff suppressed because it is too large Load Diff
-26
View File
@@ -1,26 +0,0 @@
#![allow(unused)] // TODO
use anyhow::Result;
use crossterm::event::Event;
use ratatui::Frame;
use crate::cmd::diff::AppState;
pub mod function_diff;
#[derive(Default)]
pub struct EventResult {
pub redraw: bool,
pub click_xy: Option<(u16, u16)>,
}
pub enum EventControlFlow {
Break,
Continue(EventResult),
Reload,
}
pub trait UiView {
fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult);
fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow;
fn reload(&mut self, state: &AppState) -> Result<()>;
}

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