apkdev 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apkdev/__init__.py +13 -0
- apkdev/__main__.py +5 -0
- apkdev/apkfile.py +168 -0
- apkdev/builder.py +671 -0
- apkdev/cli.py +925 -0
- apkdev/completion.py +14 -0
- apkdev/device.py +161 -0
- apkdev/inspector.py +291 -0
- apkdev/optimizer.py +58 -0
- apkdev/reverser.py +62 -0
- apkdev/sdk.py +526 -0
- apkdev/utils.py +74 -0
- apkdev-2.0.0.dist-info/METADATA +159 -0
- apkdev-2.0.0.dist-info/RECORD +17 -0
- apkdev-2.0.0.dist-info/WHEEL +5 -0
- apkdev-2.0.0.dist-info/entry_points.txt +2 -0
- apkdev-2.0.0.dist-info/top_level.txt +1 -0
apkdev/sdk.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""SDK manager โ cross-platform setup and tool checking."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from .utils import ANDROID_HOME, ensure_android_home
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def detect_platform() -> dict:
|
|
19
|
+
"""Detect platform and return package manager info."""
|
|
20
|
+
info = {
|
|
21
|
+
"system": sys.platform,
|
|
22
|
+
"is_termux": "TERMUX_VERSION" in os.environ,
|
|
23
|
+
"is_android": os.environ.get("ANDROID_ROOT") == "/system",
|
|
24
|
+
"pkg_manager": None,
|
|
25
|
+
"pkg_install_cmd": None,
|
|
26
|
+
"pkg_update_cmd": None,
|
|
27
|
+
"java_pkg": "java",
|
|
28
|
+
"home_dir": str(Path.home()),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if info["is_termux"]:
|
|
32
|
+
info["pkg_manager"] = "pkg"
|
|
33
|
+
info["pkg_install_cmd"] = ["pkg", "install", "-y"]
|
|
34
|
+
info["pkg_update_cmd"] = ["pkg", "update"]
|
|
35
|
+
info["java_pkg"] = "openjdk-21"
|
|
36
|
+
info["platform_name"] = "Termux (Android)"
|
|
37
|
+
elif sys.platform == "linux":
|
|
38
|
+
if shutil.which("apt-get"):
|
|
39
|
+
info["pkg_manager"] = "apt"
|
|
40
|
+
info["pkg_install_cmd"] = ["sudo", "apt-get", "install", "-y"]
|
|
41
|
+
info["pkg_update_cmd"] = ["sudo", "apt-get", "update"]
|
|
42
|
+
info["java_pkg"] = "openjdk-21-jdk"
|
|
43
|
+
info["platform_name"] = "Linux (Debian/Ubuntu)"
|
|
44
|
+
elif shutil.which("dnf"):
|
|
45
|
+
info["pkg_manager"] = "dnf"
|
|
46
|
+
info["pkg_install_cmd"] = ["sudo", "dnf", "install", "-y"]
|
|
47
|
+
info["java_pkg"] = "java-21-openjdk-devel"
|
|
48
|
+
info["platform_name"] = "Linux (Fedora/RHEL)"
|
|
49
|
+
elif shutil.which("pacman"):
|
|
50
|
+
info["pkg_manager"] = "pacman"
|
|
51
|
+
info["pkg_install_cmd"] = ["sudo", "pacman", "-S", "--noconfirm"]
|
|
52
|
+
info["java_pkg"] = "jdk21-openjdk"
|
|
53
|
+
info["platform_name"] = "Linux (Arch)"
|
|
54
|
+
else:
|
|
55
|
+
info["platform_name"] = "Linux (unknown package manager)"
|
|
56
|
+
elif sys.platform == "darwin":
|
|
57
|
+
if shutil.which("brew"):
|
|
58
|
+
info["pkg_manager"] = "brew"
|
|
59
|
+
info["pkg_install_cmd"] = ["brew", "install"]
|
|
60
|
+
info["java_pkg"] = "openjdk@21"
|
|
61
|
+
info["platform_name"] = "macOS (Homebrew)"
|
|
62
|
+
else:
|
|
63
|
+
info["platform_name"] = "macOS (no Homebrew)"
|
|
64
|
+
elif sys.platform == "win32":
|
|
65
|
+
if shutil.which("winget"):
|
|
66
|
+
info["pkg_manager"] = "winget"
|
|
67
|
+
info["pkg_install_cmd"] = ["winget", "install"]
|
|
68
|
+
info["java_pkg"] = "Amazon.Corretto.21"
|
|
69
|
+
info["platform_name"] = "Windows (winget)"
|
|
70
|
+
elif shutil.which("choco"):
|
|
71
|
+
info["pkg_manager"] = "choco"
|
|
72
|
+
info["pkg_install_cmd"] = ["choco", "install", "-y"]
|
|
73
|
+
info["java_pkg"] = "openjdk21"
|
|
74
|
+
info["platform_name"] = "Windows (Chocolatey)"
|
|
75
|
+
else:
|
|
76
|
+
info["platform_name"] = "Windows"
|
|
77
|
+
else:
|
|
78
|
+
info["platform_name"] = sys.platform
|
|
79
|
+
|
|
80
|
+
return info
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _version_or_none(cmd: str, *args: str) -> str:
|
|
84
|
+
"""Get version string from a tool."""
|
|
85
|
+
tool = shutil.which(cmd)
|
|
86
|
+
if not tool:
|
|
87
|
+
return "โ Not found"
|
|
88
|
+
try:
|
|
89
|
+
r = subprocess.run([tool] + list(args), capture_output=True, text=True, timeout=5)
|
|
90
|
+
out = r.stdout.split("\n")[0].strip()
|
|
91
|
+
return out[:80] if out else "โ
OK"
|
|
92
|
+
except Exception:
|
|
93
|
+
return "โ
OK"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def info() -> None:
|
|
97
|
+
"""Display environment info with platform detection."""
|
|
98
|
+
ensure_android_home()
|
|
99
|
+
plat = detect_platform()
|
|
100
|
+
|
|
101
|
+
console.print(f"[bold cyan]๐ฑ APKDev โ {plat['platform_name']}[/]")
|
|
102
|
+
console.print()
|
|
103
|
+
|
|
104
|
+
# Build tools table
|
|
105
|
+
table = Table(title="Build Tools", border_style="blue")
|
|
106
|
+
table.add_column("Tool", style="bold yellow")
|
|
107
|
+
table.add_column("Status", style="green")
|
|
108
|
+
|
|
109
|
+
table.add_row("Java", _version_or_none("java", "-version"))
|
|
110
|
+
table.add_row("Kotlin", _version_or_none("kotlin", "-version"))
|
|
111
|
+
table.add_row("Gradle", _version_or_none("gradle", "--version"))
|
|
112
|
+
table.add_row("AAPT2", _version_or_none("aapt2", "version"))
|
|
113
|
+
table.add_row("D8", _version_or_none("d8", "--version"))
|
|
114
|
+
table.add_row("ADB", _version_or_none("adb", "--version"))
|
|
115
|
+
table.add_row("Apksigner", _version_or_none("apksigner", "version"))
|
|
116
|
+
table.add_row("Zipalign", "โ
OK" if shutil.which("zipalign") else "โ Not found")
|
|
117
|
+
|
|
118
|
+
sdk = "โ Not found"
|
|
119
|
+
platforms_dir = Path(ANDROID_HOME) / "platforms"
|
|
120
|
+
if platforms_dir.exists():
|
|
121
|
+
plats = [p.name for p in platforms_dir.iterdir() if p.is_dir()]
|
|
122
|
+
sdk = ", ".join(plats) if plats else "โ No platforms"
|
|
123
|
+
table.add_row("Android SDK", sdk)
|
|
124
|
+
|
|
125
|
+
console.print(table)
|
|
126
|
+
console.print()
|
|
127
|
+
|
|
128
|
+
# RE tools table
|
|
129
|
+
table2 = Table(title="Reverse Engineering Tools", border_style="magenta")
|
|
130
|
+
table2.add_column("Tool", style="bold yellow")
|
|
131
|
+
table2.add_column("Status", style="green")
|
|
132
|
+
|
|
133
|
+
table2.add_row("Apktool", _version_or_none("apktool", "--version"))
|
|
134
|
+
table2.add_row("jadx", _version_or_none("jadx", "--version"))
|
|
135
|
+
|
|
136
|
+
smali_sh = shutil.which("smali")
|
|
137
|
+
baksmali_sh = shutil.which("baksmali")
|
|
138
|
+
table2.add_row("smali", f"โ
{smali_sh}" if smali_sh else "โ Not found")
|
|
139
|
+
table2.add_row("baksmali", f"โ
{baksmali_sh}" if baksmali_sh else "โ Not found")
|
|
140
|
+
table2.add_row("dex2jar", "โ
OK" if shutil.which("d2j-dex2jar") else "โ Not found")
|
|
141
|
+
|
|
142
|
+
aej = Path.home() / ".APKEditor.jar"
|
|
143
|
+
if aej.exists():
|
|
144
|
+
try:
|
|
145
|
+
r = subprocess.run(["java", "-jar", str(aej), "--version"],
|
|
146
|
+
capture_output=True, text=True, timeout=5)
|
|
147
|
+
table2.add_row("APKEditor", r.stdout.strip() or "โ
OK")
|
|
148
|
+
except Exception:
|
|
149
|
+
table2.add_row("APKEditor", "โ
OK")
|
|
150
|
+
else:
|
|
151
|
+
table2.add_row("APKEditor", "โ Not found")
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
__import__("androguard")
|
|
155
|
+
table2.add_row("androguard", "โ
OK")
|
|
156
|
+
except ImportError:
|
|
157
|
+
table2.add_row("androguard", "โ Not found")
|
|
158
|
+
|
|
159
|
+
console.print(table2)
|
|
160
|
+
console.print(f"\n[dim]Platform: {plat['platform_name']} | Python: {sys.version.split()[0]}[/]")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _download_jar(url: str, dest: Path) -> bool:
|
|
164
|
+
"""Download a JAR file using pure Python urllib."""
|
|
165
|
+
import urllib.request
|
|
166
|
+
try:
|
|
167
|
+
urllib.request.urlretrieve(url, str(dest))
|
|
168
|
+
return dest.exists() and dest.stat().st_size > 1000
|
|
169
|
+
except Exception:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _create_wrapper(path: Path, name: str) -> None:
|
|
174
|
+
"""Create a wrapper script (platform-aware)."""
|
|
175
|
+
import stat
|
|
176
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
if sys.platform == "win32":
|
|
178
|
+
path = path.with_suffix(".bat")
|
|
179
|
+
content = f"@echo off\njava -jar \"%USERPROFILE%\\.{name}.jar\" %*\n"
|
|
180
|
+
else:
|
|
181
|
+
content = f"#!/usr/bin/env bash\nexec java -jar \"$HOME/.{name}.jar\" \"$@\"\n"
|
|
182
|
+
path.write_text(content)
|
|
183
|
+
if sys.platform != "win32":
|
|
184
|
+
path.chmod(0o755 | stat.S_IRUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _install_java(plat: dict) -> bool:
|
|
188
|
+
"""Try to install Java via package manager.
|
|
189
|
+
Returns True if Java was installed or already present."""
|
|
190
|
+
if shutil.which("java"):
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
console.print()
|
|
194
|
+
console.print("[bold]๐ Java is required. Attempting to install...[/]")
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
if plat["is_termux"]:
|
|
198
|
+
# Termux โ no root needed
|
|
199
|
+
console.print(f" Running: pkg install {plat['java_pkg']}")
|
|
200
|
+
r = subprocess.run(["pkg", "install", "-y", plat["java_pkg"]],
|
|
201
|
+
capture_output=True, text=True, timeout=120)
|
|
202
|
+
if r.returncode == 0:
|
|
203
|
+
console.print(" โ
Java installed via pkg")
|
|
204
|
+
return shutil.which("java") is not None
|
|
205
|
+
else:
|
|
206
|
+
console.print(f" โ ๏ธ Failed: {r.stderr[-200:]}")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
elif sys.platform == "linux":
|
|
210
|
+
# Linux โ try sudo apt/dnf/pacman
|
|
211
|
+
pm = plat["pkg_install_cmd"]
|
|
212
|
+
if pm:
|
|
213
|
+
console.print(f" Running: {' '.join(pm)} {plat['java_pkg']}")
|
|
214
|
+
console.print(" [yellow](you may be prompted for sudo password)[/]")
|
|
215
|
+
r = subprocess.run(pm + [plat["java_pkg"]],
|
|
216
|
+
capture_output=True, text=True, timeout=180)
|
|
217
|
+
if r.returncode == 0:
|
|
218
|
+
console.print(" โ
Java installed")
|
|
219
|
+
return shutil.which("java") is not None
|
|
220
|
+
else:
|
|
221
|
+
console.print(f" โ ๏ธ Install may have failed: {r.stderr[-200:]}")
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
elif sys.platform == "darwin":
|
|
225
|
+
# macOS โ try Homebrew
|
|
226
|
+
brew = shutil.which("brew")
|
|
227
|
+
if not brew:
|
|
228
|
+
console.print(" Homebrew not found. Attempting to install Homebrew...")
|
|
229
|
+
console.print(" [yellow]This may take a few minutes.[/]")
|
|
230
|
+
r = subprocess.run([
|
|
231
|
+
"/bin/bash", "-c",
|
|
232
|
+
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
233
|
+
], capture_output=True, text=True, timeout=300)
|
|
234
|
+
if r.returncode == 0:
|
|
235
|
+
brew = shutil.which("brew") or "/opt/homebrew/bin/brew"
|
|
236
|
+
console.print(" โ
Homebrew installed")
|
|
237
|
+
else:
|
|
238
|
+
console.print(f" โ ๏ธ Homebrew install failed: {r.stderr[-200:]}")
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
if brew:
|
|
242
|
+
console.print(f" Running: {brew} install openjdk@21")
|
|
243
|
+
r = subprocess.run([brew, "install", "openjdk@21"],
|
|
244
|
+
capture_output=True, text=True, timeout=300)
|
|
245
|
+
if r.returncode == 0:
|
|
246
|
+
console.print(" โ
Java installed via Homebrew")
|
|
247
|
+
return shutil.which("java") is not None
|
|
248
|
+
else:
|
|
249
|
+
console.print(f" โ ๏ธ Install issue: {r.stderr[-200:]}")
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
elif sys.platform == "win32":
|
|
253
|
+
# Windows โ try Chocolatey, then winget
|
|
254
|
+
choco = shutil.which("choco") or shutil.which("choco.exe")
|
|
255
|
+
|
|
256
|
+
if not choco:
|
|
257
|
+
console.print(" Chocolatey not found. Attempting to install...")
|
|
258
|
+
console.print(" [yellow](requires PowerShell as Administrator)[/]")
|
|
259
|
+
ps_cmd = (
|
|
260
|
+
"Set-ExecutionPolicy Bypass -Scope Process -Force; "
|
|
261
|
+
"[System.Net.ServicePointManager]::SecurityProtocol = "
|
|
262
|
+
"[System.Net.ServicePointManager]::SecurityProtocol -bor 3072; "
|
|
263
|
+
"iex ((New-Object System.Net.WebClient).DownloadString("
|
|
264
|
+
"'https://chocolatey.org/install.ps1'))"
|
|
265
|
+
)
|
|
266
|
+
r = subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd],
|
|
267
|
+
capture_output=True, text=True, timeout=120)
|
|
268
|
+
if r.returncode == 0:
|
|
269
|
+
choco = shutil.which("choco") or shutil.which("choco.exe")
|
|
270
|
+
console.print(" โ
Chocolatey installed")
|
|
271
|
+
else:
|
|
272
|
+
console.print(f" โ ๏ธ Chocolatey install failed: {r.stderr[-200:]}")
|
|
273
|
+
# Try winget as fallback
|
|
274
|
+
winget = shutil.which("winget") or shutil.which("winget.exe")
|
|
275
|
+
if winget:
|
|
276
|
+
console.print(" Trying winget instead...")
|
|
277
|
+
r = subprocess.run([winget, "install", "--id", "Amazon.Corretto.21", "--accept-package-agreements"],
|
|
278
|
+
capture_output=True, text=True, timeout=120)
|
|
279
|
+
if r.returncode == 0:
|
|
280
|
+
console.print(" โ
Java installed via winget")
|
|
281
|
+
return shutil.which("java") is not None
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
if choco:
|
|
285
|
+
console.print(f" Running: choco install openjdk21")
|
|
286
|
+
r = subprocess.run([choco, "install", "openjdk21", "-y"],
|
|
287
|
+
capture_output=True, text=True, timeout=180)
|
|
288
|
+
if r.returncode == 0:
|
|
289
|
+
console.print(" โ
Java installed via Chocolatey")
|
|
290
|
+
return shutil.which("java") is not None
|
|
291
|
+
else:
|
|
292
|
+
console.print(f" โ ๏ธ Install issue: {r.stderr[-200:]}")
|
|
293
|
+
return False
|
|
294
|
+
except Exception as e:
|
|
295
|
+
console.print(f" โ ๏ธ Java install failed: {e}")
|
|
296
|
+
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def setup() -> None:
|
|
301
|
+
"""Setup: installs everything possible via Python.
|
|
302
|
+
|
|
303
|
+
What this installs:
|
|
304
|
+
- Java 17+ (via pkg/apt/brew/choco โ may need sudo/admin)
|
|
305
|
+
- androguard (via pip)
|
|
306
|
+
- smali, baksmali, APKEditor JARs (via urllib)
|
|
307
|
+
- Android SDK (if Java is available)
|
|
308
|
+
|
|
309
|
+
Run without arguments for full auto-setup.
|
|
310
|
+
"""
|
|
311
|
+
import urllib.request
|
|
312
|
+
import zipfile
|
|
313
|
+
|
|
314
|
+
plat = detect_platform()
|
|
315
|
+
console.print(f"[bold cyan]๐ฑ APKDev Setup โ {plat['platform_name']}[/]")
|
|
316
|
+
console.print()
|
|
317
|
+
|
|
318
|
+
# Step 0: Install Java (if missing)
|
|
319
|
+
has_java = shutil.which("java") is not None
|
|
320
|
+
if not has_java:
|
|
321
|
+
console.print("[bold]Step 0: Install Java 17+[/]")
|
|
322
|
+
has_java = _install_java(plat)
|
|
323
|
+
if has_java:
|
|
324
|
+
console.print(" โ
Java is ready!")
|
|
325
|
+
else:
|
|
326
|
+
console.print(" โ ๏ธ Could not auto-install Java.")
|
|
327
|
+
console.print(" Install it manually:")
|
|
328
|
+
if sys.platform == "win32":
|
|
329
|
+
console.print(" https://adoptium.net/temurin/releases/ (JDK 21)")
|
|
330
|
+
elif sys.platform == "darwin":
|
|
331
|
+
console.print(" brew install openjdk@21")
|
|
332
|
+
else:
|
|
333
|
+
console.print(" sudo apt-get install openjdk-21-jdk")
|
|
334
|
+
console.print()
|
|
335
|
+
|
|
336
|
+
# Step 0b: Install Termux build + RE tools (if on Termux)
|
|
337
|
+
if plat["is_termux"]:
|
|
338
|
+
console.print("[bold]Step 0b: Android build + RE tools[/]")
|
|
339
|
+
needed = ["aapt2", "d8", "apksigner", "jadx", "dex2jar"]
|
|
340
|
+
missing = [t for t in needed if not shutil.which(t)]
|
|
341
|
+
if missing:
|
|
342
|
+
console.print(f" Installing: {' '.join(missing)}")
|
|
343
|
+
subprocess.run(["pkg", "install", "-y"] + missing,
|
|
344
|
+
capture_output=True, timeout=120)
|
|
345
|
+
for t in missing:
|
|
346
|
+
if shutil.which(t):
|
|
347
|
+
console.print(f" โ
{t} installed")
|
|
348
|
+
else:
|
|
349
|
+
console.print(f" โ ๏ธ {t} failed")
|
|
350
|
+
else:
|
|
351
|
+
console.print(" โ
All build + RE tools already installed")
|
|
352
|
+
console.print()
|
|
353
|
+
else:
|
|
354
|
+
# Non-Termux: try to install RE tools via platform package manager
|
|
355
|
+
re_tools = {"linux": ["jadx", "dex2jar"], "darwin": ["jadx", "dex2jar"], "win32": ["jadx", "dex2jar"]}
|
|
356
|
+
console.print("[bold]Step 0b: Reverse engineering tools[/]")
|
|
357
|
+
pm = plat.get("pkg_install_cmd", [])
|
|
358
|
+
if pm:
|
|
359
|
+
missing = [t for t in re_tools.get(sys.platform, []) if not shutil.which(t)]
|
|
360
|
+
if missing:
|
|
361
|
+
console.print(f" Installing: {' '.join(missing)}")
|
|
362
|
+
try:
|
|
363
|
+
subprocess.run(pm + missing, capture_output=True, timeout=120)
|
|
364
|
+
for t in missing:
|
|
365
|
+
if shutil.which(t):
|
|
366
|
+
console.print(f" โ
{t} installed")
|
|
367
|
+
except:
|
|
368
|
+
console.print(f" โ ๏ธ Try: sudo apt-get install jadx dex2jar")
|
|
369
|
+
else:
|
|
370
|
+
console.print(" โ
RE tools already installed")
|
|
371
|
+
console.print()
|
|
372
|
+
|
|
373
|
+
python_bin_dir = Path(shutil.which("python3") or sys.executable).parent
|
|
374
|
+
|
|
375
|
+
# Step 1: smali/baksmali (Java JARs)
|
|
376
|
+
console.print("[bold]Step 1: smali/baksmali (JARs)[/]")
|
|
377
|
+
for name, jar_path, url in [
|
|
378
|
+
("smali", Path.home() / ".smali.jar",
|
|
379
|
+
"https://bitbucket.org/JesusFreke/smali/downloads/smali-2.5.2.jar"),
|
|
380
|
+
("baksmali", Path.home() / ".baksmali.jar",
|
|
381
|
+
"https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar"),
|
|
382
|
+
]:
|
|
383
|
+
if not jar_path.exists():
|
|
384
|
+
console.print(f" Downloading {name} 2.5.2...")
|
|
385
|
+
if _download_jar(url, jar_path):
|
|
386
|
+
_create_wrapper(python_bin_dir / name, name)
|
|
387
|
+
console.print(f" โ
{name} installed")
|
|
388
|
+
else:
|
|
389
|
+
console.print(f" โ ๏ธ {name} download failed")
|
|
390
|
+
else:
|
|
391
|
+
console.print(f" โ
{name} already installed")
|
|
392
|
+
|
|
393
|
+
# Step 2: APKEditor
|
|
394
|
+
console.print("[bold]Step 2: APKEditor (JAR)[/]")
|
|
395
|
+
apkeditor_jar = Path.home() / ".APKEditor.jar"
|
|
396
|
+
if not apkeditor_jar.exists() or apkeditor_jar.stat().st_size < 1000:
|
|
397
|
+
console.print(" Downloading APKEditor 1.4.9...")
|
|
398
|
+
if _download_jar(
|
|
399
|
+
"https://github.com/REAndroid/APKEditor/releases/download/V1.4.9/APKEditor-1.4.9.jar",
|
|
400
|
+
apkeditor_jar
|
|
401
|
+
):
|
|
402
|
+
_create_wrapper(python_bin_dir / "apkeditor", "APKEditor")
|
|
403
|
+
console.print(" โ
APKEditor installed")
|
|
404
|
+
else:
|
|
405
|
+
console.print(" โ ๏ธ Download failed")
|
|
406
|
+
else:
|
|
407
|
+
console.print(" โ
APKEditor already installed")
|
|
408
|
+
|
|
409
|
+
# Step 3: androguard (Python package)
|
|
410
|
+
console.print("[bold]Step 3: Python packages[/]")
|
|
411
|
+
try:
|
|
412
|
+
import androguard
|
|
413
|
+
console.print(f" โ
androguard {androguard.__version__} already")
|
|
414
|
+
except ImportError:
|
|
415
|
+
console.print(" Installing androguard via pip...")
|
|
416
|
+
try:
|
|
417
|
+
subprocess.run(
|
|
418
|
+
[sys.executable, "-m", "pip", "install", "androguard"],
|
|
419
|
+
capture_output=True, timeout=120
|
|
420
|
+
)
|
|
421
|
+
console.print(" โ
androguard installed")
|
|
422
|
+
except Exception:
|
|
423
|
+
console.print(" โ ๏ธ pip install failed, try: pip3 install androguard")
|
|
424
|
+
|
|
425
|
+
# Step 4: Android SDK
|
|
426
|
+
console.print("[bold]Step 4: Android SDK (for building)[/]")
|
|
427
|
+
sdk_dir = Path.home() / "android-sdk"
|
|
428
|
+
sdk_done = (sdk_dir / "platforms" / "android-34").exists()
|
|
429
|
+
has_java = shutil.which("java") is not None # Re-check after possible install
|
|
430
|
+
|
|
431
|
+
if not sdk_done and has_java:
|
|
432
|
+
console.print(" Setting up Android SDK...")
|
|
433
|
+
sdk_dir.mkdir(parents=True, exist_ok=True)
|
|
434
|
+
|
|
435
|
+
zip_path = Path.home() / "cmdline-tools.zip"
|
|
436
|
+
if not zip_path.exists():
|
|
437
|
+
console.print(" Downloading SDK tools (~150 MB)...")
|
|
438
|
+
try:
|
|
439
|
+
urllib.request.urlretrieve(
|
|
440
|
+
"https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip",
|
|
441
|
+
str(zip_path)
|
|
442
|
+
)
|
|
443
|
+
except Exception as e:
|
|
444
|
+
console.print(f" โ ๏ธ Download failed: {e}")
|
|
445
|
+
|
|
446
|
+
if zip_path.exists() and zip_path.stat().st_size > 1000:
|
|
447
|
+
with zipfile.ZipFile(str(zip_path), "r") as zf:
|
|
448
|
+
zf.extractall(str(sdk_dir))
|
|
449
|
+
|
|
450
|
+
cmdline_dir = sdk_dir / "cmdline-tools"
|
|
451
|
+
latest_dir = cmdline_dir / "latest"
|
|
452
|
+
if not latest_dir.exists():
|
|
453
|
+
latest_dir.mkdir(parents=True, exist_ok=True)
|
|
454
|
+
# Move bin/, lib/ etc. into latest/
|
|
455
|
+
for item in cmdline_dir.iterdir():
|
|
456
|
+
if item.name != "latest" and item.name != ".." and item.name != ".":
|
|
457
|
+
dest = latest_dir / item.name
|
|
458
|
+
if not dest.exists():
|
|
459
|
+
item.rename(dest)
|
|
460
|
+
sdkmanager = latest_dir / "bin" / "sdkmanager"
|
|
461
|
+
if sdkmanager.exists():
|
|
462
|
+
os.environ["ANDROID_HOME"] = str(sdk_dir)
|
|
463
|
+
# Ensure executable
|
|
464
|
+
sdkmanager.chmod(0o755)
|
|
465
|
+
sdk_root_arg = f"--sdk_root={sdk_dir}"
|
|
466
|
+
console.print(" Accepting SDK licenses...")
|
|
467
|
+
subprocess.run([str(sdkmanager), sdk_root_arg, "--licenses"],
|
|
468
|
+
input=b"y\ny\ny\ny\ny\n", capture_output=True, timeout=30)
|
|
469
|
+
console.print(" Installing android-34...")
|
|
470
|
+
r = subprocess.run([str(sdkmanager), sdk_root_arg, "platforms;android-34"],
|
|
471
|
+
capture_output=True, timeout=300)
|
|
472
|
+
if r.returncode == 0:
|
|
473
|
+
console.print(" โ
Android SDK installed")
|
|
474
|
+
sdk_done = True
|
|
475
|
+
else:
|
|
476
|
+
console.print(f" โ ๏ธ SDK issue: {r.stderr[-200:].decode()}")
|
|
477
|
+
elif sdk_done:
|
|
478
|
+
console.print(" โ
Android SDK already installed")
|
|
479
|
+
elif not has_java:
|
|
480
|
+
console.print(" โ ๏ธ Java needed for SDK")
|
|
481
|
+
|
|
482
|
+
# Step 5: Debug keystore
|
|
483
|
+
console.print("[bold]Step 5: Debug keystore[/]")
|
|
484
|
+
keystore = Path.home() / ".android" / "debug.keystore"
|
|
485
|
+
if not keystore.exists() and has_java:
|
|
486
|
+
console.print(" Creating debug keystore...")
|
|
487
|
+
keystore.parent.mkdir(parents=True, exist_ok=True)
|
|
488
|
+
try:
|
|
489
|
+
subprocess.run(["keytool", "-genkey", "-v", "-keystore", str(keystore),
|
|
490
|
+
"-storepass", "android", "-alias", "androiddebugkey",
|
|
491
|
+
"-keypass", "android", "-keyalg", "RSA", "-keysize", "2048",
|
|
492
|
+
"-validity", "10000",
|
|
493
|
+
"-dname", "CN=Android Debug,O=Android,C=US"],
|
|
494
|
+
capture_output=True, timeout=15)
|
|
495
|
+
console.print(" โ
Debug keystore created")
|
|
496
|
+
except Exception:
|
|
497
|
+
console.print(" โ ๏ธ keytool not available")
|
|
498
|
+
elif keystore.exists():
|
|
499
|
+
console.print(" โ
Debug keystore exists")
|
|
500
|
+
|
|
501
|
+
# Summary
|
|
502
|
+
has_java = shutil.which("java") is not None
|
|
503
|
+
console.print()
|
|
504
|
+
console.print("[bold green]โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ[/]")
|
|
505
|
+
console.print("[bold green] Setup complete![/]")
|
|
506
|
+
console.print("[bold green]โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ[/]")
|
|
507
|
+
console.print()
|
|
508
|
+
console.print(f" {'โ
' if has_java else 'โ'} Java โ {'installed' if has_java else 'install manually'}")
|
|
509
|
+
console.print(f" {'โ
' if sdk_done else 'โ'} SDK โ {'installed' if sdk_done else 'needs Java'}")
|
|
510
|
+
console.print(f" โ
apkdev โ always ready (pure Python)")
|
|
511
|
+
console.print()
|
|
512
|
+
console.print(" [bold]Commands without Java:[/] ls, extract, new, info, doctor")
|
|
513
|
+
if not has_java:
|
|
514
|
+
console.print()
|
|
515
|
+
console.print(" [yellow]To install Java manually:[/]")
|
|
516
|
+
if sys.platform == "win32":
|
|
517
|
+
console.print(" https://adoptium.net/temurin/releases/")
|
|
518
|
+
elif sys.platform == "darwin":
|
|
519
|
+
console.print(" brew install openjdk@21")
|
|
520
|
+
elif plat["is_termux"]:
|
|
521
|
+
console.print(" pkg install openjdk-21")
|
|
522
|
+
else:
|
|
523
|
+
console.print(" sudo apt-get install openjdk-21-jdk")
|
|
524
|
+
console.print()
|
|
525
|
+
if has_java and not sdk_done:
|
|
526
|
+
console.print("[yellow]Re-run after Java install: apkdev setup[/]")
|
apkdev/utils.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Utilities: subprocess helpers, env, path resolution."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
ANDROID_HOME = Path.home() / "android-sdk"
|
|
10
|
+
APKDEV_HOME = Path.home() / ".pi-tui"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_tool(name: str, hint: str = "") -> str:
|
|
14
|
+
"""Return path to a tool, or raise if not found."""
|
|
15
|
+
path = shutil.which(name)
|
|
16
|
+
if path:
|
|
17
|
+
return path
|
|
18
|
+
msg = f"โ '{name}' not found. "
|
|
19
|
+
if name == "java":
|
|
20
|
+
msg += "Java is required. Install JDK 17+ from https://adoptium.net"
|
|
21
|
+
elif name == "aapt2":
|
|
22
|
+
msg += "Install: pkg install aapt2 (Termux) or sudo apt-get install aapt2"
|
|
23
|
+
elif name == "apktool":
|
|
24
|
+
msg += "Install: pkg install apktool (Termux) or sudo apt-get install apktool"
|
|
25
|
+
elif name == "jadx":
|
|
26
|
+
msg += "Install: pkg install jadx (Termux) or sudo apt-get install jadx"
|
|
27
|
+
else:
|
|
28
|
+
msg += hint or f"Install the '{name}' package for your OS"
|
|
29
|
+
raise FileNotFoundError(msg)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
|
|
33
|
+
"""Run a command with nice error handling."""
|
|
34
|
+
try:
|
|
35
|
+
return subprocess.run(cmd, check=True, **kwargs)
|
|
36
|
+
except subprocess.CalledProcessError as e:
|
|
37
|
+
print(f"โ ๏ธ Command failed: {' '.join(cmd)}", file=sys.stderr)
|
|
38
|
+
sys.exit(e.returncode)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_java(jar: str, args: list[str], **kwargs) -> subprocess.CompletedProcess:
|
|
42
|
+
"""Run a Java JAR."""
|
|
43
|
+
java = check_tool("java")
|
|
44
|
+
return run([java, "-jar", jar] + args, **kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def ensure_android_home():
|
|
48
|
+
"""Set ANDROID_HOME if not already set."""
|
|
49
|
+
if "ANDROID_HOME" not in os.environ:
|
|
50
|
+
os.environ["ANDROID_HOME"] = str(ANDROID_HOME)
|
|
51
|
+
if "ANDROID_SDK_ROOT" not in os.environ:
|
|
52
|
+
os.environ["ANDROID_SDK_ROOT"] = str(ANDROID_HOME)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_termux() -> bool:
|
|
56
|
+
"""Check if we're running inside Termux on Android."""
|
|
57
|
+
return "TERMUX_VERSION" in os.environ or os.environ.get("ANDROID_ROOT") == "/system"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_android() -> bool:
|
|
61
|
+
"""Check if we're running on Android."""
|
|
62
|
+
return os.environ.get("ANDROID_ROOT") == "/system" or os.uname().sysname == "Linux" and os.uname().release.find("android") >= 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def banner():
|
|
66
|
+
"""Return a nice ascii banner."""
|
|
67
|
+
from rich.panel import Panel
|
|
68
|
+
from rich.text import Text
|
|
69
|
+
text = Text()
|
|
70
|
+
text.append(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n")
|
|
71
|
+
text.append(" โ ๐ฑ APKDev v2.0 โ\n")
|
|
72
|
+
text.append(" โ Build ยท Reverse ยท Engineer โ\n")
|
|
73
|
+
text.append(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n")
|
|
74
|
+
return Panel(text, style="bold cyan", border_style="cyan")
|