| #!/usr/bin/env python3 | |
| # Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file | |
| # for details. All rights reserved. Use of this source code is governed by a | |
| # BSD-style license that can be found in the LICENSE file. | |
| import os | |
| import subprocess | |
| import argparse | |
| import tempfile | |
| import zipfile | |
| from multiprocessing import Pool, cpu_count | |
| def decompile_file(args): | |
| """Decompiles a single class file using javap -v -p.""" | |
| class_file, output_file = args | |
| # Ensure the output directory exists. os.makedirs(exist_ok=True) is process-safe | |
| # in Python 3.2+, so multiple workers can call this without a race condition. | |
| os.makedirs(os.path.dirname(output_file), exist_ok=True) | |
| try: | |
| with open(output_file, 'w') as f: | |
| subprocess.run(['javap', '-v', '-p', class_file], | |
| stdout=f, | |
| stderr=subprocess.PIPE, | |
| check=True) | |
| return True | |
| except subprocess.CalledProcessError as e: | |
| print(f"Error decompiling {class_file}: {e.stderr.decode().strip()}") | |
| return False | |
| except Exception as e: | |
| print(f"Unexpected error with {class_file}: {e}") | |
| return False | |
| def process_classes(classes_dir, output_dir, jobs): | |
| """Processes all class files in the given directory.""" | |
| tasks = [] | |
| for root, _, files in os.walk(classes_dir): | |
| for file in files: | |
| if file.endswith(".class"): | |
| class_path = os.path.join(root, file) | |
| rel_path = os.path.relpath(class_path, classes_dir) | |
| output_path = os.path.join( | |
| output_dir, | |
| os.path.splitext(rel_path)[0] + ".javap") | |
| tasks.append((class_path, output_path)) | |
| if not tasks: | |
| print("No .class files found.") | |
| return | |
| print( | |
| f"Found {len(tasks)} files. Decompiling using {jobs} parallel jobs...") | |
| with Pool(jobs) as pool: | |
| results = pool.map(decompile_file, tasks) | |
| success_count = sum(results) | |
| print( | |
| f"Finished. Successfully decompiled {success_count} / {len(tasks)} files." | |
| ) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Decompile Java class files using javap -v -p.") | |
| parser.add_argument( | |
| "classes", help="Directory containing .class files or a .jar file.") | |
| parser.add_argument("output", | |
| help="Directory to save decompiled .javap files.") | |
| parser.add_argument("-j", | |
| "--jobs", | |
| type=int, | |
| default=cpu_count(), | |
| help="Number of parallel jobs (default: all CPUs).") | |
| args = parser.parse_args() | |
| input_path = os.path.abspath(args.classes) | |
| output_dir = os.path.abspath(args.output) | |
| if not os.path.exists(input_path): | |
| print(f"Error: Input path '{input_path}' does not exist.") | |
| return | |
| if os.path.isfile(input_path) and input_path.endswith(".jar"): | |
| print(f"Input is a JAR file. Extracting {input_path}...") | |
| with tempfile.TemporaryDirectory() as tmp_dir: | |
| try: | |
| with zipfile.ZipFile(input_path, 'r') as jar: | |
| jar.extractall(tmp_dir) | |
| process_classes(tmp_dir, output_dir, args.jobs) | |
| except zipfile.BadZipFile: | |
| print(f"Error: '{input_path}' is not a valid JAR/ZIP file.") | |
| elif os.path.isdir(input_path): | |
| process_classes(input_path, output_dir, args.jobs) | |
| else: | |
| print(f"Error: '{input_path}' is neither a directory nor a .jar file.") | |
| if __name__ == "__main__": | |
| main() |