"` / `exec --file ` | **UNSAFE**: run Python in the backend process. | `--file`, same |
| `read int/string/struct [...]` | Typed reads: decode memory as an integer, C string, or defined struct. | `--size`, `--signed`, `--endian`, `--max-len`, `--encoding`, same + `--json` |
| `read_memory ` | Read raw bytes from the binary at ``. Default output is a hexdump. | `--format {hexdump,hex,raw}`, same + `--json` (base64-encoded bytes) |
+| `backend status jadx` | Check the optional Java/JADX runtime without loading an input. | `--json` |
| `install-skill` | Install this file for Claude Code or Codex. | `--agent`, `--dest`, `--force`, `--json` |
### `xref_to` vs `get_callers`
diff --git a/docs/decompiler_cli.md b/docs/decompiler_cli.md
index dd7de33a..965c00b0 100644
--- a/docs/decompiler_cli.md
+++ b/docs/decompiler_cli.md
@@ -31,6 +31,7 @@ and can be installed with `decompiler install-skill`.
- [`rename`](#rename)
- [`list_strings`](#list_strings)
- [`get_callers`](#get_callers)
+ - [`backend status`](#backend-status)
- [`install-skill`](#install-skill)
- [Server selection (`--id`, `--binary`, `--backend`)](#server-selection)
- [JSON output (`--json`, `--raw`)](#json-output)
@@ -65,9 +66,11 @@ Pick a backend you have available:
- **ghidra** — requires `GHIDRA_INSTALL_DIR` and uses PyGhidra.
- **binja** — requires a Binary Ninja license.
- **ida** — requires IDA Pro.
-- **jadx** — requires Java 17+ and Gradle for the first worker build. It uses
- stable JVM/Dex references and has dedicated `class`, `method`, `field`,
- `resource`, and `manifest` commands; see the
+- **jadx** — optional; requires the official JADX 1.5.6+ distribution and
+ Java 17+. DecLib finds it via `JADX_HOME` or `jadx` on `PATH`. Check setup
+ with `decompiler backend status jadx --json`. It uses stable JVM/Dex
+ references and has dedicated `class`, `method`, `field`, `resource`, and
+ `manifest` commands; see the
[JADX backend guide](./jadx.md).
---
@@ -658,6 +661,19 @@ to the backend's lifted form, so both refer to the same byte instead of the
backend double-adding the image base. An address at or above the image base is
treated as absolute; a smaller one is already lifted.
+### `backend status`
+
+Check whether an optional backend runtime is installed without starting a
+decompiler server:
+
+```bash
+decompiler backend status jadx [--json]
+```
+
+The JSON response reports the bridge JAR, Java runtime, official JADX JAR and
+detected versions. It exits successfully when the backend is ready and exits
+`1` with a `reasons` list when setup is incomplete.
+
### `install-skill`
Copy the bundled Agent Skill into a supported agent skill directory so Claude
diff --git a/docs/jadx.md b/docs/jadx.md
index c22ccb8c..00b4a603 100644
--- a/docs/jadx.md
+++ b/docs/jadx.md
@@ -7,23 +7,28 @@ the worker's standard input and output.
## Setup
-The prototype worker requires Java 17 or newer and Gradle. It is built
-automatically on the first JADX load:
+JADX is optional and is not bundled with DecLib. Install the official JADX
+1.5.6 or newer distribution and Java 17 or newer. DecLib finds JADX through
+`JADX_HOME` or a `jadx` executable on `PATH`:
```bash
+export JADX_HOME=/opt/jadx
+decompiler backend status jadx --json
decompiler load ./challenge.apk --backend jadx
```
-To build it ahead of time:
+Released DecLib wheels contain the small Java bridge used to communicate with
+JADX. Gradle is only needed when developing DecLib from a source checkout and
+the bridge has not been built:
```bash
-cd declib/decompilers/jadx/worker
-gradle --no-daemon test installDist
+gradle --no-daemon -p declib/decompilers/jadx/worker test jar
```
-Set `DECLIB_JADX_WORKER` to use a prebuilt worker command. The generated
-launcher accepts additional JVM arguments through
-`DECLIB_JADX_WORKER_OPTS`. Its default maximum heap is 4 GiB:
+Set `DECLIB_JADX_JAR` to select a specific official `jadx-*-all.jar`, or
+`DECLIB_JADX_WORKER` to use a completely custom worker command. Additional
+JVM arguments can be supplied through `DECLIB_JADX_WORKER_OPTS`; the default
+maximum heap is 4 GiB:
```bash
export DECLIB_JADX_WORKER_OPTS="-Xmx8g"
diff --git a/pyproject.toml b/pyproject.toml
index 8edf0d10..c7814f96 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,7 +56,9 @@ include-package-data = true
"declib.decompilers.jadx.worker" = [
"build.gradle.kts",
"settings.gradle.kts",
+ "bridge/declib-jadx-worker.jar",
"src/main/java/**/*.java",
+ "src/main/resources/**/*",
]
[tool.setuptools.packages]
diff --git a/tests/test_jadx.py b/tests/test_jadx.py
index 9dfc0703..51e9557d 100644
--- a/tests/test_jadx.py
+++ b/tests/test_jadx.py
@@ -1,5 +1,7 @@
+import os
import shlex
import sys
+from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
@@ -112,3 +114,82 @@ def test_worker_client_json_protocol(tmp_path):
}
with pytest.raises(ValueError, match="bad input"):
client.call("fail")
+
+
+def test_find_official_jadx_runtime_from_home(tmp_path, monkeypatch):
+ jadx_home = tmp_path / "jadx"
+ jadx_jar = jadx_home / "lib" / "jadx-1.5.6-all.jar"
+ jadx_jar.parent.mkdir(parents=True)
+ jadx_jar.write_bytes(b"test")
+ monkeypatch.setenv("JADX_HOME", str(jadx_home))
+ monkeypatch.delenv("DECLIB_JADX_JAR", raising=False)
+ monkeypatch.setattr("shutil.which", lambda name: None)
+
+ assert JadxWorkerClient.find_jadx_runtime() == (jadx_jar, "1.5.6")
+
+
+def test_find_official_jadx_runtime_rejects_old_version(tmp_path, monkeypatch):
+ jadx_home = tmp_path / "jadx"
+ jadx_jar = jadx_home / "lib" / "jadx-1.5.5-all.jar"
+ jadx_jar.parent.mkdir(parents=True)
+ jadx_jar.write_bytes(b"test")
+ monkeypatch.setenv("JADX_HOME", str(jadx_home))
+ monkeypatch.delenv("DECLIB_JADX_JAR", raising=False)
+ monkeypatch.setattr("shutil.which", lambda name: None)
+
+ with pytest.raises(RuntimeError, match="too old"):
+ JadxWorkerClient.find_jadx_runtime()
+
+
+def test_resolve_command_uses_thin_bridge_and_official_jadx(
+ tmp_path,
+ monkeypatch,
+):
+ java = tmp_path / "java"
+ bridge = tmp_path / "declib-jadx-worker.jar"
+ jadx = tmp_path / "jadx-1.5.6-all.jar"
+ for path in (java, bridge, jadx):
+ path.write_bytes(b"test")
+
+ monkeypatch.delenv("DECLIB_JADX_WORKER", raising=False)
+ monkeypatch.setenv("DECLIB_JADX_WORKER_OPTS", "-Xmx2g")
+ with (
+ patch.object(
+ JadxWorkerClient,
+ "find_jadx_runtime",
+ return_value=(jadx, "1.5.6"),
+ ),
+ patch.object(
+ JadxWorkerClient,
+ "find_java",
+ return_value=(java, 21),
+ ),
+ patch.object(
+ JadxWorkerClient,
+ "find_bridge_jar",
+ return_value=bridge,
+ ),
+ ):
+ command = JadxWorkerClient.resolve_command(build_if_missing=False)
+
+ assert command == [
+ str(java),
+ "-Xmx2g",
+ "-cp",
+ f"{bridge}{os.pathsep}{jadx}",
+ JadxWorkerClient.WORKER_MAIN_CLASS,
+ ]
+
+
+def test_runtime_status_reports_missing_optional_runtime(monkeypatch):
+ monkeypatch.delenv("DECLIB_JADX_WORKER", raising=False)
+ with (
+ patch.object(JadxWorkerClient, "find_bridge_jar", return_value=Path("/bridge.jar")),
+ patch.object(JadxWorkerClient, "find_java", return_value=(Path("/java"), 21)),
+ patch.object(JadxWorkerClient, "find_jadx_runtime", return_value=(None, None)),
+ ):
+ status = JadxWorkerClient.runtime_status()
+
+ assert status["available"] is False
+ assert status["source"] == "official-jadx"
+ assert status["reasons"] == ["official JADX runtime was not found"]
From 44275b8bf613aff2337e0d3bef8b703ba2eb3935 Mon Sep 17 00:00:00 2001
From: Eric Gustafson
Date: Thu, 23 Jul 2026 22:22:27 -0700
Subject: [PATCH 3/4] Allow decompiler CI to run without proprietary secrets
---
.github/workflows/dec-tests.yml | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/dec-tests.yml b/.github/workflows/dec-tests.yml
index 52144ea7..7ae436f4 100644
--- a/.github/workflows/dec-tests.yml
+++ b/.github/workflows/dec-tests.yml
@@ -27,14 +27,16 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install .[test]
- - name: Download BS Artifact & Install IDA
+ - name: Download BS test artifacts
+ run: git clone https://github.com/binsync/bs-artifacts.git /tmp/bs-artifacts
+ - name: Install IDA
+ if: ${{ env.TOOLING_KEY != '' }}
run: |
- (git clone https://github.com/binsync/bs-artifacts.git /tmp/bs-artifacts && \
- cd /tmp/bs-artifacts && \
- ./helpers/setup_ida_ci.sh)
+ cd /tmp/bs-artifacts
+ ./helpers/setup_ida_ci.sh
# taken from https://github.com/mandiant/capa/blob/master/.github/workflows/tests.yml#L107-L147
- name: Install Binary Ninja
- if: ${{ env.BN_SERIAL != 0 }}
+ if: ${{ env.BN_SERIAL != '' && env.BN_LICENSE != '' }}
run: |
mkdir ./.github/binja
curl "https://raw.githubusercontent.com/Vector35/binaryninja-api/6812c97/scripts/download_headless.py" -o ./.github/binja/download_headless.py
@@ -62,10 +64,8 @@ jobs:
auth_token: ${{ secrets.GITHUB_TOKEN }}
- name: Pytest
run: |
- # these two test must be run in separate python environments, due to the way ghidra bridge works
- # you also must run these tests in the exact order shown here
- TEST_BINARIES_DIR=/tmp/bs-artifacts/binaries pytest \
- tests/test_decompilers.py \
- tests/test_client_server.py \
- tests/test_decompiler_cli.py \
- -sv
+ tests=(tests/test_client_server.py tests/test_decompiler_cli.py)
+ if [[ -n "$TOOLING_KEY" && -n "$BN_SERIAL" && -n "$BN_LICENSE" ]]; then
+ tests=(tests/test_decompilers.py "${tests[@]}")
+ fi
+ TEST_BINARIES_DIR=/tmp/bs-artifacts/binaries pytest "${tests[@]}" -sv
From 35633e8534b76423387f458d378076d9201eff5f Mon Sep 17 00:00:00 2001
From: Eric Gustafson
Date: Fri, 24 Jul 2026 10:00:59 -0700
Subject: [PATCH 4/4] Revert "Allow decompiler CI to run without proprietary
secrets"
This reverts commit 44275b8bf613aff2337e0d3bef8b703ba2eb3935.
---
.github/workflows/dec-tests.yml | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/dec-tests.yml b/.github/workflows/dec-tests.yml
index 7ae436f4..52144ea7 100644
--- a/.github/workflows/dec-tests.yml
+++ b/.github/workflows/dec-tests.yml
@@ -27,16 +27,14 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install .[test]
- - name: Download BS test artifacts
- run: git clone https://github.com/binsync/bs-artifacts.git /tmp/bs-artifacts
- - name: Install IDA
- if: ${{ env.TOOLING_KEY != '' }}
+ - name: Download BS Artifact & Install IDA
run: |
- cd /tmp/bs-artifacts
- ./helpers/setup_ida_ci.sh
+ (git clone https://github.com/binsync/bs-artifacts.git /tmp/bs-artifacts && \
+ cd /tmp/bs-artifacts && \
+ ./helpers/setup_ida_ci.sh)
# taken from https://github.com/mandiant/capa/blob/master/.github/workflows/tests.yml#L107-L147
- name: Install Binary Ninja
- if: ${{ env.BN_SERIAL != '' && env.BN_LICENSE != '' }}
+ if: ${{ env.BN_SERIAL != 0 }}
run: |
mkdir ./.github/binja
curl "https://raw.githubusercontent.com/Vector35/binaryninja-api/6812c97/scripts/download_headless.py" -o ./.github/binja/download_headless.py
@@ -64,8 +62,10 @@ jobs:
auth_token: ${{ secrets.GITHUB_TOKEN }}
- name: Pytest
run: |
- tests=(tests/test_client_server.py tests/test_decompiler_cli.py)
- if [[ -n "$TOOLING_KEY" && -n "$BN_SERIAL" && -n "$BN_LICENSE" ]]; then
- tests=(tests/test_decompilers.py "${tests[@]}")
- fi
- TEST_BINARIES_DIR=/tmp/bs-artifacts/binaries pytest "${tests[@]}" -sv
+ # these two test must be run in separate python environments, due to the way ghidra bridge works
+ # you also must run these tests in the exact order shown here
+ TEST_BINARIES_DIR=/tmp/bs-artifacts/binaries pytest \
+ tests/test_decompilers.py \
+ tests/test_client_server.py \
+ tests/test_decompiler_cli.py \
+ -sv