diff --git a/skills/apk-reverse/references/toolchain.md b/skills/apk-reverse/references/toolchain.md index 73ee17c..13e3dee 100644 --- a/skills/apk-reverse/references/toolchain.md +++ b/skills/apk-reverse/references/toolchain.md @@ -56,6 +56,13 @@ python scripts/rasc_build.py --build # needs git + rustup; ~2 min python scripts/rasc_build.py --verify app.apk # compares class sets against droidasc, fails on a difference ``` +**It is a registered capability, so G2 does not have to guess:** `doctor.py` reports `dex_index_rust` +as `OK` when a binary is present (PATH, `APKREV_TOOLS`, or the work-area path `rasc_build.py` builds +into) and `BLOCKED` with that build command as the next action when it is not — alongside +`dex_index_python` for the `pip install` route. A machine with neither is not stuck: the Python +indexer is one command away, and the two are cross-checked against each other by +`rasc_build.py --verify`. + **The one thing to know before trusting its `getclass`:** on an `enum` whose constants override an abstract method, the outer class is printed as a bare constant list and the per-constant bodies are **not inlined** — no warning, and none in the Python tool either, because both leave those bodies in diff --git a/skills/apk-reverse/scripts/capabilities.py b/skills/apk-reverse/scripts/capabilities.py index 8e3d80a..e5b8a15 100644 --- a/skills/apk-reverse/scripts/capabilities.py +++ b/skills/apk-reverse/scripts/capabilities.py @@ -190,6 +190,27 @@ ATOMS = { 'est_minutes': 5, 'basis': 'unverified', }, + 'tool:rasc': { + 'name': 'rasc (Rust ASC: whole-APK dex index)', + 'kind': 'tool', + 'tool': 'rasc', + 'install': 'it ships no artifact -- run `python scripts/rasc_build.py --build` ' + '(needs git plus rustup; on Windows the GNU host toolchain also needs a ' + '64-bit MinGW-w64 gcc), or set RASC to an existing binary', + 'est_minutes': 5, + # Measured here: a clean clone built in 117 s once the toolchain existed, which is the + # toolchain-acquisition cost on top. The toolchain itself is the larger part and is + # machine-specific, so the estimate covers the build step and says so. + 'basis': 'measured', + }, + 'tool:droidasc': { + 'name': 'droidasc (Python ASC: whole-APK dex index)', + 'kind': 'tool', + 'tool': 'droidasc', + 'install': 'pip install droidasc', + 'est_minutes': 1, + 'basis': 'measured', + }, 'tool:apktool': { 'name': 'apktool (resource round-trip)', 'kind': 'tool', @@ -317,6 +338,24 @@ GATE_FILES = { # `depends_on` is expanded recursively, so a prerequisite's blockers are inherited. CAPABILITIES = { + 'dex_index_rust': { + 'name': 'Whole-APK dex index and single-class decompilation (rasc, the Rust ASC)', + 'required': ['python39', 'tool:rasc'], + 'optional': [], + 'scripts': ['rasc_build.py'], + 'note': 'the fastest indexer this kit documents -- same answers as droidasc on ' + 'both archives measured, 4-15x quicker -- but it ships no prebuilt ' + 'artifact, so `rasc_build.py --build` is how a machine acquires it. Its ' + 'outer-class view of an enum does not inline the constant bodies; the ' + 'subclasses decompile fine (references/rasc-and-droidsaw.md)', + }, + 'dex_index_python': { + 'name': 'Whole-APK dex index and single-class decompilation (droidasc, the Python ASC)', + 'required': ['python39', 'tool:droidasc'], + 'optional': [], + 'note': 'one `pip install droidasc` away, and the cross-check for the Rust one: ' + 'their class-definition sets are compared in scripts/rasc_build.py --verify', + }, 'static_dex': { 'name': 'Static dex / zip / strings analysis', 'required': ['python39'], @@ -520,6 +559,18 @@ class Probe: if name in self._tool_cache: return self._tool_cache[name] hit = shutil.which(name) + if not hit and name == 'rasc': + # rasc ships no artifact, so a machine that followed this kit's own build step has it + # under the repository's ignored work area rather than on PATH. Accepting that location + # keeps the capability table from reporting BLOCKED at an agent that already did the + # work -- the same reasoning as APKREV_TOOLS, for the one tool whose install step is a + # build (scripts/rasc_build.py). + for cand in (os.path.join(REPO_ROOT, 'tools', '_work', 'rust', 'target', 'release', + 'rasc' + ('.exe' if os.name == 'nt' else '')), + os.environ.get('RASC', '')): + if cand and os.path.isfile(cand): + hit = cand + break if not hit: exts = [''] if os.name != 'nt' else ['.exe', '.bat', '.cmd', '.ps1', ''] for d in extra_tool_dirs(): diff --git a/skills/apk-reverse/scripts/doctor.py b/skills/apk-reverse/scripts/doctor.py index 90512c6..88645da 100644 --- a/skills/apk-reverse/scripts/doctor.py +++ b/skills/apk-reverse/scripts/doctor.py @@ -561,7 +561,10 @@ def main(): 'blocked': counts['blocked'], 'unknown': counts['unknown']}, } print(json.dumps(payload, indent=2, ensure_ascii=False)) - print('RESULT=%s' % token) + # No `RESULT=` line on this path: appending one makes the stream invalid JSON for a caller + # doing `--json | jq` or `json.loads(stdout)`, which is what `--json` is for. The status is a + # field in the document, and the exit code carries the same meaning. (Same defect that was + # found in check_commands.py, and it was found here by trying to parse this output.) return code print('=' * 74)