damon-devtools 0.3.0__tar.gz → 0.4.0__tar.gz

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.
Files changed (29) hide show
  1. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/PKG-INFO +1 -1
  2. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/damon_devtools.egg-info/PKG-INFO +1 -1
  3. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/apkpull/core.py +162 -5
  4. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/apkpull/ui.py +35 -6
  5. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/pyproject.toml +1 -1
  6. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/README.md +0 -0
  7. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/damon_devtools.egg-info/SOURCES.txt +0 -0
  8. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/damon_devtools.egg-info/dependency_links.txt +0 -0
  9. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/damon_devtools.egg-info/entry_points.txt +0 -0
  10. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/damon_devtools.egg-info/requires.txt +0 -0
  11. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/damon_devtools.egg-info/top_level.txt +0 -0
  12. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/__init__.py +0 -0
  13. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/__main__.py +0 -0
  14. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/launcher.py +0 -0
  15. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/__init__.py +0 -0
  16. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/apkpull/__init__.py +0 -0
  17. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/apkpull/__main__.py +0 -0
  18. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/apkpull/models.py +0 -0
  19. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/portkill/__init__.py +0 -0
  20. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/portkill/__main__.py +0 -0
  21. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/portkill/core.py +0 -0
  22. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/portkill/models.py +0 -0
  23. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/portkill/ui.py +0 -0
  24. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/removebg/__init__.py +0 -0
  25. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/removebg/__main__.py +0 -0
  26. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/removebg/cli.py +0 -0
  27. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/tools/removebg/core.py +0 -0
  28. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/devtools/types.py +0 -0
  29. {damon_devtools-0.3.0 → damon_devtools-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: damon-devtools
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Developer CLI tools — portkill, removebg, apkpull
5
5
  Author: damon
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: damon-devtools
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Developer CLI tools — portkill, removebg, apkpull
5
5
  Author: damon
6
6
  License: MIT
@@ -17,6 +17,15 @@ AAPT2_REMOTE_URL = "https://github.com/JonForShort/android-tools/raw/refs/heads/
17
17
  # Device-side aapt2 path
18
18
  AAPT2_DEVICE_PATH = "/data/local/tmp/aapt2"
19
19
 
20
+ # Apktool download URL and local path
21
+ APKTOOL_VERSION = "2.12.1"
22
+ APKTOOL_REMOTE_URL = f"https://github.com/iBotPeaches/Apktool/releases/download/v{APKTOOL_VERSION}/apktool_{APKTOOL_VERSION}.jar"
23
+ APKTOOL_LOCAL_DIR = os.path.join(
24
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
25
+ "deps", "apktool"
26
+ )
27
+ APKTOOL_JAR = os.path.join(APKTOOL_LOCAL_DIR, f"apktool_{APKTOOL_VERSION}.jar")
28
+
20
29
  # App name cache
21
30
  LABEL_CACHE_FILE = os.path.expanduser("~/.devtools/cache/apkpull/label_cache.json")
22
31
 
@@ -215,19 +224,25 @@ def scan_packages() -> list[str]:
215
224
  return pkgs
216
225
 
217
226
  def get_apk_path(pkg: str) -> str | None:
218
- """执行 pm path,返回 APK 物理路径"""
227
+ """执行 pm path,返回 APK 物理路径(仅返回第一个)"""
228
+ paths = get_apk_paths(pkg)
229
+ return paths[0] if paths else None
230
+
231
+ def get_apk_paths(pkg: str) -> list[str]:
232
+ """执行 pm path,返回所有 APK 物理路径(split APK 会返回多个)"""
219
233
  serial = get_device_serial()
220
234
  try:
221
235
  result = subprocess.run(
222
236
  adb_cmd(["shell", "pm", "path", pkg], serial),
223
237
  capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
224
238
  )
239
+ paths = []
225
240
  for line in result.stdout.strip().split("\n"):
226
241
  if line.startswith("package:"):
227
- return line[8:].strip()
228
- return None
242
+ paths.append(line[8:].strip())
243
+ return paths
229
244
  except (subprocess.CalledProcessError, FileNotFoundError):
230
- return None
245
+ return []
231
246
 
232
247
  def get_app_name_pm_dump(pkg: str) -> str:
233
248
  """通过设备端 pm dump 提取 application label(失败时回退包名)"""
@@ -300,4 +315,146 @@ def pull_apk(apk_path: str, dest_path: str) -> bool:
300
315
  )
301
316
  return True
302
317
  except (subprocess.CalledProcessError, FileNotFoundError):
303
- return False
318
+ return False
319
+
320
+ def download_apktool() -> str | None:
321
+ """Download apktool jar and return local path, or None on failure."""
322
+ if os.path.isfile(APKTOOL_JAR):
323
+ return APKTOOL_JAR
324
+ try:
325
+ os.makedirs(APKTOOL_LOCAL_DIR, exist_ok=True)
326
+ print(f"正在下载 apktool {APKTOOL_VERSION}...")
327
+ urllib.request.urlretrieve(APKTOOL_REMOTE_URL, APKTOOL_JAR)
328
+ return APKTOOL_JAR
329
+ except Exception as e:
330
+ print(f"apktool 下载失败: {e}")
331
+ return None
332
+
333
+ def ensure_apktool() -> str | None:
334
+ """Ensure apktool is available, download if missing, and initialize framework."""
335
+ # Check if apktool.jar exists locally
336
+ if not os.path.isfile(APKTOOL_JAR):
337
+ if not download_apktool():
338
+ return None
339
+
340
+ # Initialize apktool framework (one-time setup)
341
+ # --force ensures the framework is updated to support newer Android SDK attributes
342
+ try:
343
+ subprocess.run(
344
+ ["java", "-jar", APKTOOL_JAR, "empty-framework-dir"],
345
+ capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60
346
+ )
347
+ except Exception:
348
+ pass # Non-fatal, continue even if cf fails
349
+
350
+ return APKTOOL_JAR
351
+
352
+ def merge_split_apks(apk_paths: list[str], output_apk: str, java_path: str | None = None) -> bool:
353
+ """
354
+ Merge multiple split APKs into a single APK using apktool.
355
+ apk_paths: list of local APK file paths (first is treated as base)
356
+ output_apk: path to write the merged APK
357
+ Returns True on success.
358
+ """
359
+ import shutil
360
+
361
+ java = java_path or "java"
362
+ apktool_jar = ensure_apktool()
363
+ if not apktool_jar:
364
+ print("错误: apktool 不可用")
365
+ return False
366
+
367
+ # Check java
368
+ try:
369
+ subprocess.run([java, "-version"], capture_output=True, check=True)
370
+ except (subprocess.CalledProcessError, FileNotFoundError):
371
+ print("错误: 未找到 Java,请确保已安装 JDK 并加入 PATH")
372
+ return False
373
+
374
+ base_apk = apk_paths[0]
375
+ split_apks = apk_paths[1:]
376
+
377
+ # Create temp working directory
378
+ work_dir = tempfile.mkdtemp(prefix="apkmerge_")
379
+ base_dir = os.path.join(work_dir, "base")
380
+ decoded_dir = base_dir # apktool will create this
381
+
382
+ try:
383
+ # Decode base APK
384
+ print(f"正在解码基础 APK...")
385
+ result = subprocess.run(
386
+ [java, "-jar", apktool_jar, "d", base_apk, "-o", base_dir, "-f"],
387
+ capture_output=True, text=True, encoding="utf-8", errors="replace"
388
+ )
389
+ if result.returncode != 0:
390
+ print(f"apktool 解码失败: {result.stderr}")
391
+ return False
392
+
393
+ # Decode and merge each split APK
394
+ for split_apk in split_apks:
395
+ print(f"正在解码分片 APK: {os.path.basename(split_apk)}")
396
+ split_dir = tempfile.mkdtemp(prefix="split_")
397
+ result = subprocess.run(
398
+ [java, "-jar", apktool_jar, "d", split_apk, "-o", split_dir, "-f"],
399
+ capture_output=True, text=True, encoding="utf-8", errors="replace"
400
+ )
401
+ if result.returncode != 0:
402
+ print(f"apktool 解码分片失败: {result.stderr}")
403
+ return False
404
+
405
+ # Copy dex files from split to base (apktool names them classes.dex, classes2.dex, etc.)
406
+ split_root = split_dir
407
+ for fname in os.listdir(split_root):
408
+ if fname.endswith(".dex"):
409
+ src = os.path.join(split_root, fname)
410
+ dst = os.path.join(base_dir, fname)
411
+ # If already exists (from another split), rename (classes3.dex, etc.)
412
+ if os.path.exists(dst):
413
+ base_name = fname.rsplit(".", 1)[0]
414
+ ext = fname.rsplit(".", 1)[1]
415
+ counter = 2
416
+ while os.path.exists(dst):
417
+ dst = os.path.join(base_dir, f"{base_name}{counter}.{ext}")
418
+ counter += 1
419
+ shutil.copy2(src, dst)
420
+ print(f" 合并: {fname} -> {os.path.basename(dst)}")
421
+
422
+ # Copy lib directories if present (architecture-specific libs)
423
+ for lib_name in ["lib", "assets"]:
424
+ split_lib = os.path.join(split_dir, lib_name)
425
+ if os.path.isdir(split_lib):
426
+ base_lib = os.path.join(base_dir, lib_name)
427
+ if not os.path.isdir(base_lib):
428
+ shutil.copytree(split_lib, base_lib)
429
+ else:
430
+ # Merge contents
431
+ for item in os.listdir(split_lib):
432
+ src_item = os.path.join(split_lib, item)
433
+ dst_item = os.path.join(base_lib, item)
434
+ if os.path.isdir(src_item):
435
+ if not os.path.exists(dst_item):
436
+ shutil.copytree(src_item, dst_item)
437
+ else:
438
+ if not os.path.exists(dst_item):
439
+ shutil.copy2(src_item, dst_item)
440
+ shutil.rmtree(split_dir)
441
+
442
+ # Build merged APK
443
+ print("正在构建合并 APK...")
444
+ result = subprocess.run(
445
+ [java, "-jar", apktool_jar, "b", base_dir, "-o", output_apk],
446
+ capture_output=True, text=True, encoding="utf-8", errors="replace"
447
+ )
448
+ if result.returncode != 0:
449
+ print(f"apktool 构建失败: {result.stderr}")
450
+ return False
451
+
452
+ print(f"合并完成: {output_apk}")
453
+ return True
454
+
455
+ except Exception as e:
456
+ print(f"合并过程出错: {e}")
457
+ return False
458
+ finally:
459
+ # Cleanup
460
+ shutil.rmtree(work_dir, ignore_errors=True)
@@ -158,21 +158,50 @@ def run(args: list[str] | None = None):
158
158
  continue
159
159
 
160
160
  # Pull APKs
161
- from devtools.tools.apkpull.core import get_apk_path, pull_apk, sanitize_filename
161
+ from devtools.tools.apkpull.core import get_apk_paths, pull_apk, merge_split_apks, sanitize_filename
162
162
  succeeded: list[str] = []
163
163
  failed: list[tuple] = []
164
164
 
165
165
  for p in selected:
166
- apk_path = get_apk_path(p.pkg)
167
- if not apk_path:
166
+ apk_paths = get_apk_paths(p.pkg)
167
+ if not apk_paths:
168
168
  failed.append((p.label, "未找到 APK 路径"))
169
169
  continue
170
170
  file_base = p.label if p.label and p.label != p.pkg else p.pkg
171
171
  dest_apk = os.path.join(parsed.dest_dir, f"{sanitize_filename(file_base)}.apk")
172
- if pull_apk(apk_path, dest_apk):
173
- succeeded.append(p.label)
172
+
173
+ if len(apk_paths) == 1:
174
+ # Single APK - direct pull
175
+ if pull_apk(apk_paths[0], dest_apk):
176
+ succeeded.append(p.label)
177
+ else:
178
+ failed.append((p.label, "拉取失败"))
174
179
  else:
175
- failed.append((p.label, "拉取失败"))
180
+ # Split APK - pull all then merge
181
+ print(f"\n检测到 split APK (共 {len(apk_paths)} 个分片),正在拉取并合并...")
182
+ tmp_apks: list[str] = []
183
+ tmp_dir = os.path.join(parsed.dest_dir, f".tmp_{sanitize_filename(file_base)}")
184
+ try:
185
+ os.makedirs(tmp_dir, exist_ok=True)
186
+ all_ok = True
187
+ for i, apk_path in enumerate(apk_paths):
188
+ tmp_file = os.path.join(tmp_dir, f"split_{i}.apk")
189
+ if not pull_apk(apk_path, tmp_file):
190
+ failed.append((p.label, f"拉取分片 {i+1} 失败"))
191
+ all_ok = False
192
+ break
193
+ tmp_apks.append(tmp_file)
194
+
195
+ if all_ok:
196
+ if merge_split_apks(tmp_apks, dest_apk):
197
+ succeeded.append(p.label)
198
+ else:
199
+ failed.append((p.label, "合并 APK 失败"))
200
+ finally:
201
+ # Cleanup temp files
202
+ import shutil
203
+ if os.path.isdir(tmp_dir):
204
+ shutil.rmtree(tmp_dir, ignore_errors=True)
176
205
 
177
206
  print()
178
207
  if succeeded:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "damon-devtools"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Developer CLI tools — portkill, removebg, apkpull"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
File without changes
File without changes