@harmonyos-arkts/d2h 0.1.20 → 0.1.22

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 (152) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +40 -0
  3. package/agents/android-to-hmos-00-orchestrator.md +3 -315
  4. package/agents/d2h.md +53 -38
  5. package/contracts/workflow/artifacts.md +22 -0
  6. package/contracts/workflow/delivery.md +54 -0
  7. package/contracts/workflow/dispatch.md +26 -0
  8. package/contracts/workflow/execution.md +125 -0
  9. package/contracts/workflow/generation.md +40 -0
  10. package/contracts/workflow/orchestration.md +49 -0
  11. package/contracts/workflow/overview.md +58 -0
  12. package/contracts/workflow/plan.md +119 -0
  13. package/contracts/workflow/scope.md +151 -0
  14. package/dist/bin/install-opencode.js +34 -15
  15. package/dist/src/install-codex.js +131 -0
  16. package/dist/src/install-opencode.js +5 -5
  17. package/dist/src/workflow.js +54 -0
  18. package/docs/architecture-plan-guide.zh-CN.md +70 -0
  19. package/docs/incremental-spec-evaluation-design.md +670 -0
  20. package/docs/spec-plan-guide.md +226 -0
  21. package/docs/workflow-decoupling.md +59 -0
  22. package/opencode/interactive-tools/advance-progress-demo.mjs +21 -0
  23. package/opencode/interactive-tools/browser-host.ts +110 -0
  24. package/opencode/interactive-tools/core.ts +242 -0
  25. package/opencode/interactive-tools/demo-workflow.ts +55 -0
  26. package/opencode/interactive-tools/demo.mjs +37 -0
  27. package/opencode/interactive-tools/package-lock.json +1876 -0
  28. package/opencode/interactive-tools/package.json +23 -0
  29. package/opencode/interactive-tools/panel.ts +40 -0
  30. package/opencode/interactive-tools/prepare-demo.mjs +17 -0
  31. package/opencode/interactive-tools/prepare-progress-demo.mjs +13 -0
  32. package/opencode/interactive-tools/progress.ts +118 -0
  33. package/opencode/interactive-tools/server-legacy.mjs +2 -0
  34. package/opencode/interactive-tools/server.mjs +40 -0
  35. package/opencode/interactive-tools/tui.tsx +230 -0
  36. package/package.json +9 -1
  37. package/schemas/native-libs-analysis-report.schema.json +695 -0
  38. package/schemas/native-libs-verify-report.schema.json +566 -0
  39. package/schemas/spec-native-libs-shard.schema.json +110 -0
  40. package/skills/arkts-icon-sizing/SKILL.md +0 -10
  41. package/skills/cpp/d2h-native-library-analyse/SKILL.md +91 -0
  42. package/skills/cpp/d2h-native-library-analyse/scripts/analyse-android-so.py +630 -0
  43. package/skills/cpp/d2h-native-library-build/SKILL.md +403 -0
  44. package/skills/cpp/d2h-native-library-build/references/build-config-mapping.md +180 -0
  45. package/skills/cpp/d2h-native-library-build/references/jni-to-napi-mapping.md +74 -0
  46. package/skills/cpp/d2h-native-library-build/scripts/check-android-so.py +580 -0
  47. package/skills/cpp/d2h-native-library-verify/SKILL.md +111 -0
  48. package/skills/cpp/d2h-native-library-verify/references/demo-build-sign-run.md +67 -0
  49. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/app.json5 +1 -0
  50. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/resources/base/element/string.json +1 -0
  51. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/README.md +25 -0
  52. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/build-profile.json5 +24 -0
  53. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/build-profile.json5 +1 -0
  54. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/hvigorfile.ts +2 -0
  55. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/oh-package.json5 +1 -0
  56. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/entryability/EntryAbility.ets +5 -0
  57. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/Index.ets +3 -0
  58. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/NativeVerifyDemo.ets +276 -0
  59. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/module.json5 +1 -0
  60. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/color.json +1 -0
  61. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/string.json +1 -0
  62. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/profile/main_pages.json +1 -0
  63. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/FFmpegVerify.test.ets +176 -0
  64. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/List.test.ets +2 -0
  65. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/module.json5 +1 -0
  66. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigor/hvigor-config.json5 +1 -0
  67. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigorfile.ts +2 -0
  68. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/oh-package.json5 +9 -0
  69. package/skills/cpp/d2h-native-library-verify/references/demo-template.md +176 -0
  70. package/skills/cpp/d2h-native-library-verify/references/hypium-test-template.md +163 -0
  71. package/skills/cpp/d2h-native-library-verify/scripts/__pycache__/native_common.cpython-313.pyc +0 -0
  72. package/skills/cpp/d2h-native-library-verify/scripts/device-verify.py +304 -0
  73. package/skills/cpp/d2h-native-library-verify/scripts/native_common.py +341 -0
  74. package/skills/cpp/d2h-native-library-verify/scripts/verify-ohos-native.py +1099 -0
  75. package/skills/d2h-app-solution/SKILL.md +198 -0
  76. package/skills/d2h-app-solution/agents/openai.yaml +4 -0
  77. package/skills/d2h-app-solution/references/framework-evidence.md +117 -0
  78. package/skills/d2h-app-solution/references/hierarchy-rules.md +88 -0
  79. package/skills/d2h-app-solution/references/inventory-schema.md +305 -0
  80. package/skills/d2h-app-solution/references/technical-analysis.md +101 -0
  81. package/skills/d2h-app-solution/scripts/scan_repositories.py +350 -0
  82. package/skills/d2h-app-solution/scripts/validate_inventory.py +337 -0
  83. package/skills/d2h-architecture-plan/SKILL.md +35 -0
  84. package/skills/d2h-architecture-plan/references/directory-creation.md +43 -0
  85. package/skills/d2h-architecture-plan/references/hybrid-design.md +84 -0
  86. package/skills/d2h-architecture-plan/references/layout-reference.md +83 -0
  87. package/skills/d2h-architecture-plan/references/plan-contract.md +51 -0
  88. package/skills/d2h-architecture-plan/references/source-review.md +50 -0
  89. package/skills/d2h-cloud-knowledge/SKILL.md +28 -7
  90. package/skills/d2h-cloud-knowledge/kb_query.mjs +8 -0
  91. package/skills/d2h-dashboard/SKILL.md +56 -0
  92. package/skills/d2h-product-family-migration/SKILL.md +103 -0
  93. package/skills/d2h-product-family-migration/agents/openai.yaml +3 -0
  94. package/skills/d2h-product-family-migration/assets/incremental-plan.template.md +55 -0
  95. package/skills/d2h-product-family-migration/references/advanced/behavioral-verification.md +59 -0
  96. package/skills/d2h-product-family-migration/references/advanced/existing-skill-integration.md +30 -0
  97. package/skills/d2h-product-family-migration/references/advanced/packaging-and-verification.md +43 -0
  98. package/skills/d2h-product-family-migration/references/advanced/platform-probes.md +26 -0
  99. package/skills/d2h-product-family-migration/references/advanced/product-line-design.md +40 -0
  100. package/skills/d2h-product-family-migration/references/baseline-and-delta.md +50 -0
  101. package/skills/d2h-product-family-migration/tests/markdown-cases.md +76 -0
  102. package/skills/d2h-spec-develop/SKILL.md +41 -0
  103. package/skills/d2h-spec-develop/agents/openai.yaml +7 -0
  104. package/skills/d2h-spec-develop/references/worker-workflow.md +26 -0
  105. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/SKILL.md +24 -13
  106. package/skills/d2h-spec-generate/assets/spec-template.md +109 -0
  107. package/skills/d2h-spec-generate/references/from-plan.md +19 -0
  108. package/skills/d2h-spec-generate/references/spec-content.md +34 -0
  109. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/scripts/build_batch_report.py +35 -84
  110. package/skills/d2h-spec-plan/SKILL.md +38 -0
  111. package/skills/d2h-spec-plan/agents/openai.yaml +4 -0
  112. package/skills/d2h-spec-plan/references/architecture-handoff.md +9 -0
  113. package/skills/d2h-spec-plan/references/plan-contract.md +13 -0
  114. package/skills/d2h-spec-plan/references/planning-method.md +46 -0
  115. package/skills/d2h-spec-plan/references/round-scope-assessment.md +49 -0
  116. package/skills/d2h-spec-plan/references/work-packets.md +61 -0
  117. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +2 -2
  118. package/skills/d2h-utg-ai-generate/references/page-schema.json +2 -2
  119. package/skills/d2h-utg-ai-generate/references/utg-schema.json +2 -2
  120. package/skills/d2h-utg-static-generate/SKILL.md +2 -2
  121. package/skills/managing-d2h-toolchain/environment-requirements.json +1 -1
  122. package/skills/managing-d2h-toolchain/toolchain.json +1 -1
  123. package/skills/preparing-migration-workspace/SKILL.md +32 -2
  124. package/workflow/__pycache__/artifact_bindings.cpython-313.pyc +0 -0
  125. package/workflow/__pycache__/spec_runtime.cpython-313.pyc +0 -0
  126. package/workflow/artifact_bindings.py +224 -0
  127. package/workflow/assets/dashboard.html +88 -0
  128. package/workflow/build_batch_report.py +8 -0
  129. package/workflow/collect_code_metrics.py +75 -0
  130. package/workflow/dashboard_overview.py +109 -0
  131. package/workflow/delivery_view.py +82 -0
  132. package/workflow/feature_plan.py +234 -0
  133. package/workflow/page_batches.py +223 -0
  134. package/workflow/spec_generation.py +120 -0
  135. package/workflow/spec_runtime.py +890 -0
  136. package/workflow/work_packets.py +85 -0
  137. package/skills/d2h-page-develop/SKILL.md +0 -126
  138. package/skills/d2h-page-develop/agents/openai.yaml +0 -7
  139. package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
  140. package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
  141. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
  142. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
  143. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
  144. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
  145. package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
  146. package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
  147. package/skills/native-library-substitution/SKILL.md +0 -385
  148. package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
  149. package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
  150. package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
  151. package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
  152. package/skills/porting-cmake-native-libraries/references/report-and-build.md +0 -60
@@ -0,0 +1,630 @@
1
+ #!/usr/bin/env python3
2
+ """Android 应用功能清单 → JNI 方法 → 涉及的 .so/.a 文件。
3
+
4
+ 自顶向下分析:
5
+ ① 从 Android 源码提取应用**功能清单**(按 Activity/类/包分组)
6
+ ② 每个功能 → 用到的 JNI native 方法(含调用链/操作步骤)
7
+ ③ 每个 JNI 方法 → 涉及的 .so/.a 文件(通过 loadLibrary / JNI 符号 / CMake 依赖)
8
+
9
+ 输出:HTML(给人看,功能维度) + JSON(给 AI 映射,机器可读)。
10
+ """
11
+
12
+ import argparse
13
+ import html as _html
14
+ import json
15
+ import os
16
+ import re
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ from concurrent.futures import ThreadPoolExecutor
21
+
22
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../d2h-native-library-verify/scripts"))
23
+ import native_common as nc
24
+
25
+ __version__ = "3.0.0"
26
+
27
+ EXCLUDE_DIRS = {
28
+ ".git", ".hg", ".svn", "node_modules", "oh_modules", ".hvigor",
29
+ ".gradle", "build", ".idea", ".cxx", ".externalNativeBuild",
30
+ }
31
+ JAVA_EXT = (".java", ".kt")
32
+ SCRIPT_CAP = 1 << 20
33
+
34
+ # JNI/native 方法声明
35
+ JAVA_NATIVE_RE = re.compile(r"\bnative\s+[\w<>\[\],\s]*?(\w+)\s*\(")
36
+ KOTLIN_EXTERNAL_RE = re.compile(r"\bexternal\s+fun\s+(\w+)\s*\(")
37
+ PACKAGE_RE = re.compile(r"^\s*package\s+([\w.]+)", re.MULTILINE)
38
+ LOAD_LIB_RE = re.compile(r'System\.loadLibrary\("([^"]+)"\)')
39
+ CLASS_RE = re.compile(r"(?:public\s+)?(?:abstract\s+)?(?:final\s+)?class\s+(\w+)")
40
+ ACTIVITY_RE = re.compile(r"extends\s+(?:AppCompat)?Activity|@Entry")
41
+ FRAGMENT_RE = re.compile(r"extends\s+Fragment")
42
+
43
+ # Activity/场景 → 用户操作场景名(从 Activity 类名提取具体场景)
44
+ ACTIVITY_SCENARIOS = {
45
+ "VideoHandle": ("视频剪辑处理", "剪辑/转码/添加水印/调节速度等"),
46
+ "VideoPreview": ("视频预览播放", "预览视频文件/滤镜效果"),
47
+ "Player": ("视频播放器", "播放视频文件/网络流"),
48
+ "AudioHandle": ("音频剪辑处理", "音频裁剪/合成/变速等"),
49
+ "AudioPlay": ("音频播放器", "播放音频文件"),
50
+ "AudioEffect": ("音频特效", "均衡器/混响/可视化频谱"),
51
+ "Equalizer": ("均衡器调节", "调节音频频段"),
52
+ "Filter": ("视频滤镜", "给视频添加滤镜效果"),
53
+ "CameraFilter": ("相机实时滤镜", "相机预览实时滤镜"),
54
+ "Live": ("直播推流", "摄像头采集→编码→RTMP推流"),
55
+ "ProbeFormat": ("媒体格式探测", "获取媒体文件格式信息"),
56
+ "Main": ("主页入口", "应用主界面"),
57
+ }
58
+
59
+ # JNI 符号 → so 匹配
60
+ JAVA_SYMBOL_PREFIX = "Java_"
61
+ NDK_LIBS = {"libc++_shared.so", "liblog.so", "libandroid.so", "libm.so", "libc.so", "libdl.so", "libz.so"}
62
+
63
+
64
+ def read_file(path, cap=SCRIPT_CAP):
65
+ try:
66
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
67
+ return f.read(cap)
68
+ except OSError:
69
+ return ""
70
+
71
+
72
+ def run(cmd):
73
+ return nc.run(cmd)
74
+
75
+
76
+ def sdk_bin_dirs():
77
+ return nc.sdk_bin_dirs()
78
+
79
+
80
+ def resolve_tool(names):
81
+ return nc.resolve_tool(names)
82
+
83
+
84
+ TOOL_INSTALL_HINTS = nc.TOOL_INSTALL_HINTS
85
+
86
+
87
+ def _is_elf_nm(nm_path):
88
+ return nc._is_elf_nm(nm_path)
89
+
90
+
91
+ def find_elf_nm():
92
+ return nc.find_elf_nm()
93
+
94
+
95
+ def find_so_exports(so_path, nm_tool):
96
+ """返回 .so 的动态导出符号集合。"""
97
+ out = run([nm_tool, "-D", "--defined-only", "--extern-only", "-j", so_path])
98
+ return set(s.strip() for s in out.splitlines() if s.strip())
99
+
100
+
101
+ def find_project_libs(root):
102
+ """找工程中所有 .so/.a 文件,返回 {basename: full_path}。"""
103
+ libs = {}
104
+ for dirpath, dirnames, filenames in os.walk(root):
105
+ dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
106
+ for fn in filenames:
107
+ if fn.endswith((".so", ".a")):
108
+ libs[fn] = os.path.join(dirpath, fn)
109
+ return libs
110
+
111
+
112
+ def scan_sources(root):
113
+ """扫描所有 Java/Kotlin 源文件,返回文件信息列表。"""
114
+ sources = []
115
+ for dirpath, dirnames, filenames in os.walk(root):
116
+ dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
117
+ for fn in filenames:
118
+ if fn.endswith(JAVA_EXT):
119
+ sources.append(os.path.join(dirpath, fn))
120
+ return sources
121
+
122
+
123
+ def find_operation_for_method(content_lines, method_name, native_lines):
124
+ """在全文件中找 native 方法的调用点 → 外层 public 方法 = 用户操作入口。
125
+
126
+ 排除 native 声明行本身(相邻 native 方法不是操作入口)。
127
+ """
128
+ call_re = re.compile(r"\b" + re.escape(method_name) + r"\s*\(")
129
+ native_decl_re = re.compile(r"\bnative\b|\bexternal\b")
130
+
131
+ best = ""
132
+ for i, line in enumerate(content_lines):
133
+ # 跳过声明行本身
134
+ if native_decl_re.search(line):
135
+ continue
136
+ # 跳过注释
137
+ stripped = line.strip()
138
+ if stripped.startswith("//") or stripped.startswith("*") or stripped.startswith("/*"):
139
+ continue
140
+ # 找调用点
141
+ if call_re.search(line):
142
+ # 向上找最近的 public 方法
143
+ for j in range(i, max(i - 20, -1), -1):
144
+ l = content_lines[j]
145
+ # 排除 native 声明行
146
+ if native_decl_re.search(l):
147
+ continue
148
+ # 匹配 public/internal 方法签名
149
+ m2 = re.search(
150
+ r"(?:public|internal)\s+(?:static\s+)?(?:final\s+)?(?:suspend\s+)?"
151
+ r"(?:fun\s+)?[\w<>\[\],\s]*?\s+(\w+)\s*\(", l)
152
+ if m2 and m2.group(1) != method_name:
153
+ best = m2.group(1)
154
+ break
155
+ if best:
156
+ break
157
+ return best
158
+
159
+
160
+ def classify_scenario(activity_name):
161
+ """从 Activity 类名提取用户场景。"""
162
+ stem = activity_name.replace("Activity", "").replace("activity", "")
163
+ if stem in ACTIVITY_SCENARIOS:
164
+ return ACTIVITY_SCENARIOS[stem]
165
+ return (stem, stem) # 未识别的用类名本身
166
+
167
+
168
+ def classify_feature(class_name, method_name=""):
169
+ """根据 Java 类名推断所属场景——先精确匹配功能类(native_common),再模糊匹配 Activity。"""
170
+ feat = nc.classify_feature(class_name)
171
+ if feat:
172
+ return feat
173
+ # 模糊匹配 Activity 名(作为兜底)
174
+ cls = (class_name or "").lower()
175
+ for key, scenario in ACTIVITY_SCENARIOS.items():
176
+ if key.lower() in cls:
177
+ return scenario[0]
178
+ return "其他"
179
+
180
+
181
+ def extract_file_info(src_path):
182
+ """从单个源文件提取:类名、native 声明、loadLibrary、调用上下文。"""
183
+ content = read_file(src_path)
184
+ if not content:
185
+ return None
186
+ pkg_m = PACKAGE_RE.search(content)
187
+ pkg = pkg_m.group(1) if pkg_m else ""
188
+ stem = os.path.splitext(os.path.basename(src_path))[0]
189
+ full_class = (pkg + "." + stem) if pkg else stem
190
+ ext = os.path.splitext(src_path)[1].lower()
191
+
192
+ info = {
193
+ "file": os.path.basename(src_path),
194
+ "path": src_path,
195
+ "class": full_class,
196
+ "language": "java" if ext == ".java" else "kotlin",
197
+ "load_libraries": LOAD_LIB_RE.findall(content),
198
+ "native_methods": [],
199
+ "is_activity": bool(ACTIVITY_RE.search(content)),
200
+ "feature": classify_feature(full_class),
201
+ }
202
+
203
+ lines = content.splitlines()
204
+ for lineno, line in enumerate(lines, 1):
205
+ method = None
206
+ if ext == ".java":
207
+ m = JAVA_NATIVE_RE.search(line)
208
+ if m: method = m.group(1)
209
+ else:
210
+ m = KOTLIN_EXTERNAL_RE.search(line)
211
+ if m: method = m.group(1)
212
+ if method:
213
+ info["native_methods"].append({
214
+ "method": method,
215
+ "line": lineno,
216
+ "feature": classify_feature(full_class, method) or info["feature"] or "通用",
217
+ "_file": os.path.basename(src_path),
218
+ })
219
+ return info
220
+
221
+
222
+ def build_cmake_dep_map(root):
223
+ """解析 CMakeLists.txt 构建 target → 依赖列表映射(去注释后多行匹配)。"""
224
+ dep_map = {}
225
+ for dirpath, dirnames, filenames in os.walk(root):
226
+ dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
227
+ for fn in filenames:
228
+ if fn not in ("CMakeLists.txt",):
229
+ continue
230
+ raw = read_file(os.path.join(dirpath, fn))
231
+ lines = []
232
+ for line in raw.splitlines():
233
+ idx = line.find("#")
234
+ if idx >= 0:
235
+ line = line[:idx]
236
+ if line.strip():
237
+ lines.append(line)
238
+ content = "\n".join(lines)
239
+
240
+ # 找所有 add_library 的 target 名
241
+ targets = re.findall(r"add_library\s*\(\s*([\w-]+)", content)
242
+ # 找所有 target_link_libraries
243
+ for m in re.finditer(r"target_link_libraries\s*\(\s*([\w-]+)\s+([^)]*)\)", content):
244
+ target = m.group(1)
245
+ deps = []
246
+ for dep in m.group(2).split():
247
+ dep = dep.strip()
248
+ if dep.startswith("-l"):
249
+ dep = "lib" + dep[2:]
250
+ if not dep or dep.startswith("$") or dep.startswith("-"):
251
+ continue
252
+ if dep in ("log", "android", "jnigraphics", "OpenSLES", "mediandk",
253
+ "EGL", "GLESv2", "GLESv3"):
254
+ continue
255
+ deps.append(dep)
256
+ if deps:
257
+ dep_map[target] = deps
258
+ return dep_map
259
+
260
+
261
+ def match_jni_to_so(native_methods, class_name, load_libraries, project_libs, so_exports_cache, nm_tool, cmake_deps, all_sources_info=None):
262
+ """把 JNI 方法关联到 .so 文件——通过 loadLibrary 名 → CMake 依赖 → 实际文件。"""
263
+ results = []
264
+ for nm in native_methods:
265
+ method = nm["method"]
266
+
267
+ # 策略1:loadLibrary 名 → CMake 依赖 → 工程内实际 .so/.a 文件
268
+ matched_libs = []
269
+ for lib_name in load_libraries:
270
+ # loadLibrary("media-handle") → CMake target "media-handle" → depends ["ffmpeg"]
271
+ cmake_target = lib_name
272
+ deps = cmake_deps.get(cmake_target, [])
273
+ # 也查是否有 lib{lib_name}.so 直接存在
274
+ direct = "lib" + lib_name + ".so"
275
+ if direct in project_libs:
276
+ matched_libs.append({"name": direct, "match_type": "load_library", "path": project_libs[direct]})
277
+ # CMake 依赖名 → 匹配工程内文件
278
+ for dep in deps:
279
+ for suffix in (".so", ".a"):
280
+ candidate = dep + suffix
281
+ if candidate in project_libs:
282
+ matched_libs.append({"name": candidate, "match_type": "cmake_dep", "path": project_libs[candidate]})
283
+ candidate2 = "lib" + dep + suffix
284
+ if candidate2 in project_libs and candidate2 not in [m["name"] for m in matched_libs]:
285
+ matched_libs.append({"name": candidate2, "match_type": "cmake_dep", "path": project_libs[candidate2]})
286
+
287
+ # 策略1b:类自身没 loadLibrary → 扫同包/同模块其他类的 loadLibrary
288
+ if not matched_libs:
289
+ for other_info in all_sources_info:
290
+ if other_info.get("load_libraries") and not other_info.get("native_methods"):
291
+ # 这个类有 loadLibrary 但没 native 方法——可能是 LibraryLoader
292
+ other_libs = other_info["load_libraries"]
293
+ for other_lib in other_libs:
294
+ deps = cmake_deps.get(other_lib, [])
295
+ for dep in deps:
296
+ for suffix in (".so", ".a"):
297
+ cand = dep + suffix
298
+ if cand in project_libs:
299
+ matched_libs.append({"name": cand, "match_type": "indirect_load", "path": project_libs[cand]})
300
+ cand2 = "lib" + dep + suffix
301
+ if cand2 in project_libs and cand2 not in [m["name"] for m in matched_libs]:
302
+ matched_libs.append({"name": cand2, "match_type": "indirect_load", "path": project_libs[cand2]})
303
+
304
+ # 策略1c:同 app 模块(同包前缀)的其他类有 loadLibrary → 关联
305
+ if not matched_libs:
306
+ # 找同包前缀(前 3 段相同)且有 loadLibrary 的类
307
+ cls_parts = class_name.split(".")
308
+ pkg_prefix = ".".join(cls_parts[:3]) if len(cls_parts) > 3 else class_name
309
+ for other_info in all_sources_info:
310
+ other_cls = other_info.get("class", "")
311
+ if other_cls.startswith(pkg_prefix) and other_info.get("load_libraries"):
312
+ for other_lib in other_info["load_libraries"]:
313
+ deps = cmake_deps.get(other_lib, [])
314
+ for dep in deps:
315
+ for suffix in (".so", ".a"):
316
+ cand = "lib" + dep + suffix
317
+ if cand in project_libs and cand not in [m["name"] for m in matched_libs]:
318
+ matched_libs.append({"name": cand, "match_type": "same_module", "path": project_libs[cand]})
319
+
320
+ # 策略2:JNI 符号直接在 .so 导出表匹配
321
+ if not matched_libs:
322
+ parts = class_name.split(".")
323
+ jni_symbol = "Java_" + "_".join(parts) + "_" + method
324
+ for lib_name, lib_path in project_libs.items():
325
+ if lib_name.endswith(".a"):
326
+ continue
327
+ if lib_name not in so_exports_cache and nm_tool:
328
+ so_exports_cache[lib_name] = find_so_exports(lib_path, nm_tool)
329
+ if jni_symbol in so_exports_cache.get(lib_name, set()):
330
+ matched_libs.append({"name": lib_name, "match_type": "jni_symbol", "path": lib_path})
331
+
332
+ results.append({
333
+ **nm,
334
+ "class": class_name,
335
+ "file": nm.get("_file", ""),
336
+ "jni_symbol": "Java_" + "_".join(class_name.split(".")) + "_" + method,
337
+ "matched_libs": matched_libs,
338
+ })
339
+ return results
340
+
341
+
342
+
343
+ def build_feature_manifest(sources_info, project_libs, nm_tool, cmake_deps, root):
344
+ """构建功能清单:feature → JNI 方法 → so/a 文件。"""
345
+ so_exports_cache = {}
346
+ features = {}
347
+
348
+ for info in sources_info:
349
+ if not info["native_methods"]:
350
+ continue
351
+ # 为每个 native 方法计算 operation(找调用点的 public 方法)
352
+ content_lines = read_file(info["path"]).splitlines()
353
+ native_line_set = {nm["line"] for nm in info["native_methods"]}
354
+ for nm in info["native_methods"]:
355
+ nm["operation"] = find_operation_for_method(content_lines, nm["method"], native_line_set)
356
+
357
+ # 匹配 JNI → so
358
+ matched = match_jni_to_so(info["native_methods"], info["class"], info["load_libraries"], project_libs, so_exports_cache, nm_tool, cmake_deps, sources_info)
359
+
360
+ for m in matched:
361
+ feature = m["feature"]
362
+ if feature not in features:
363
+ features[feature] = {
364
+ "feature": feature,
365
+ "jni_methods": [],
366
+ "involved_libs": set(),
367
+ "source_files": set(),
368
+ }
369
+ features[feature]["jni_methods"].append({
370
+ "class": m["class"],
371
+ "method": m["method"],
372
+ "jni_symbol": m["jni_symbol"],
373
+ "file": m["file"],
374
+ "line": m["line"],
375
+ "operation": m.get("operation", "") or m["method"],
376
+ "libs": [l["name"] for l in m["matched_libs"]],
377
+ })
378
+ for l in m["matched_libs"]:
379
+ features[feature]["involved_libs"].add(l["name"])
380
+ features[feature]["source_files"].add(info["file"])
381
+
382
+ # 排序 + 清理
383
+ result = []
384
+ for feat_name in sorted(features):
385
+ f = features[feat_name]
386
+ result.append({
387
+ "feature": feat_name,
388
+ "description": f"涉及 {len(f['jni_methods'])} 个 JNI 方法,{len(f['involved_libs'])} 个原生库",
389
+ "jni_methods": sorted(f["jni_methods"], key=lambda x: (x["class"], x["method"])),
390
+ "involved_libs": sorted(f["involved_libs"]),
391
+ "source_files": sorted(f["source_files"]),
392
+ })
393
+ return result
394
+
395
+
396
+ HTML_CSS = """<style>
397
+ body{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;margin:0;background:#f6f7f9;color:#1a1a2e}
398
+ .hd{background:linear-gradient(135deg,#1a2a4a,#2d4a7a);color:#fff;padding:22px 30px}
399
+ .hd h1{margin:0 0 6px;font-size:21px}
400
+ .hd .m{font-size:12px;opacity:.85}
401
+ .ct{max-width:1100px;margin:18px auto;padding:0 14px}
402
+ details{background:#fff;border-radius:10px;margin-bottom:10px;box-shadow:0 1px 3px rgba(0,0,0,.08);overflow:hidden}
403
+ summary{padding:14px 16px;cursor:pointer;font-weight:600;font-size:15px;display:flex;align-items:center;gap:10px}
404
+ summary::-webkit-details-marker{display:none}
405
+ .bd{padding:0 16px 14px}
406
+ table{width:100%;border-collapse:collapse;font-size:13px;margin:6px 0}
407
+ th,td{text-align:left;padding:6px 10px;border-bottom:1px solid #eee;vertical-align:top}
408
+ th{color:#555;font-weight:600;background:#fafafa;font-size:12px}
409
+ .b{display:inline-block;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600}
410
+ .bg{background:#e6f7e6;color:#1a7a1a}.bl{background:#e8f0fe;color:#1a56c4}.by{background:#fff7e0;color:#9a6d00}
411
+ .tag{display:inline-block;margin:2px 3px 2px 0;padding:2px 8px;border-radius:4px;font-size:11px;background:#f0f2f5}
412
+ code{background:#f0f2f5;padding:1px 5px;border-radius:4px;font-size:12px}
413
+ .mu{color:#888;font-size:11px}
414
+ .cards{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px}
415
+ .card{background:#fff;border-radius:10px;padding:12px 18px;box-shadow:0 1px 3px rgba(0,0,0,.08);min-width:120px}
416
+ .card .n{font-size:26px;font-weight:700}
417
+ .card .l{font-size:11px;color:#666}
418
+ </style>"""
419
+
420
+
421
+ def _e(v):
422
+ return nc.esc(v)
423
+
424
+
425
+ def render_html(payload):
426
+ features = payload.get("features", [])
427
+ total_jni = sum(len(f["jni_methods"]) for f in features)
428
+ total_libs = set()
429
+ for f in features:
430
+ total_libs.update(f["involved_libs"])
431
+
432
+ h = ['<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8">',
433
+ '<meta name="viewport" content="width=device-width,initial-scale=1">',
434
+ '<title>Native 分析报告</title>', HTML_CSS,
435
+ '</head><body><div class="hd">',
436
+ '<h1>Native 分析报告</h1>',
437
+ '<div class="m">%s · 功能 %d · JNI %d · 原生库 %d</div></div><div class="ct">' % (
438
+ _e(payload.get("tool_version")), len(features), total_jni, len(total_libs))]
439
+
440
+ # 总览卡片
441
+ h.append('<div class="cards">')
442
+ for label, val, color in [("功能", len(features), "#1a56c4"), ("JNI 方法", total_jni, "#1a7a1a"),
443
+ ("涉及 .so/.a", len(total_libs), "#c0392b")]:
444
+ h.append('<div class="card"><div class="n" style="color:%s">%d</div><div class="l">%s</div></div>' % (color, val, label))
445
+ h.append('</div>')
446
+
447
+ # 📦 所有 .so/.a 文件完整清单
448
+ if payload.get("all_libs"):
449
+ h.append('<details open><summary>📦 工程内全部 .so/.a 文件(' + str(len(payload.get('all_libs', []))) + ' 个)</summary><div class="bd">')
450
+ h.append('<table><tr><th>文件名</th><th>类型</th><th>架构</th><th>检测到的能力</th><th>被哪些功能使用</th></tr>')
451
+ lib_to_features = {}
452
+ for f in features:
453
+ for lib in f["involved_libs"]:
454
+ lib_to_features.setdefault(lib, []).append(f["feature"])
455
+ for lib in payload["all_libs"]:
456
+ name = os.path.basename(lib["path"])
457
+ used_by = lib_to_features.get(name, [])
458
+ used_html = " ".join('<span class="tag" style="background:#e8f0fe;color:#1a56c4">%s</span>' % _e(f) for f in used_by) if used_by else '<span class="mu">未被直接使用</span>'
459
+ kind_label = "静态库" if lib.get("kind") == "a" else "动态库"
460
+ caps = lib.get("detected", {})
461
+ caps_html = " ".join('<span class="tag" style="background:#e6f7e6;color:#1a7a1a">%s</span>' % _e(c) for c in caps) if caps else '<span class="mu">未检测到</span>'
462
+ h.append('<tr><td><b>%s</b></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>' % (
463
+ _e(name), kind_label, _e(lib.get("arch", "?")), caps_html, used_html))
464
+ h.append('</table></div></details>')
465
+
466
+ # 功能 → so/a 总表
467
+ h.append('<details open><summary>📋 功能 → 原生库 对照表</summary><div class="bd">')
468
+ h.append('<table><tr><th>功能</th><th>JNI 方法数</th><th>涉及的 .so/.a</th></tr>')
469
+ for f in features:
470
+ libs_html = " ".join('<span class="tag">%s</span>' % _e(l) for l in f["involved_libs"])
471
+ h.append('<tr><td><b>%s</b></td><td>%d</td><td>%s</td></tr>' % (
472
+ _e(f["feature"]), len(f["jni_methods"]), libs_html))
473
+ h.append('</table></div></details>')
474
+
475
+ # 每个功能的详细卡片
476
+ for f in features:
477
+ h.append('<details%s><summary>%s <span class="b bl">%d JNI</span> %s</summary><div class="bd">' % (
478
+ "", _e(f["feature"]), len(f["jni_methods"]),
479
+ " ".join('<span class="tag">%s</span>' % _e(l) for l in f["involved_libs"])))
480
+ h.append('<table><tr><th>Android 类.方法</th><th>操作入口</th><th>源码位置</th></tr>')
481
+ for jm in f["jni_methods"]:
482
+ h.append('<tr><td><b>%s.%s()</b></td><td><code>%s</code></td><td class="mu">%s:%s</td></tr>' % (
483
+ _e(jm["class"]), _e(jm["method"]), _e(jm.get("operation", "")),
484
+ _e(jm.get("file", "")), _e(jm.get("line", ""))))
485
+ h.append('</table>')
486
+
487
+ # so/a 文件
488
+ if f["involved_libs"]:
489
+ h.append('<div style="margin-top:8px"><b>涉及原生库:</b>%s</div>' % " ".join(
490
+ '<span class="tag" style="background:#fdeaea;color:#c0392b">%s</span>' % _e(l) for l in f["involved_libs"]))
491
+
492
+ h.append('</div></details>')
493
+
494
+ h.append('</div></body></html>')
495
+ return "\n".join(h)
496
+
497
+
498
+ def _get_lib_arch(path):
499
+ """快速读 ELF 头获取架构(不重跑完整分析)。"""
500
+ try:
501
+ with open(path, "rb") as f:
502
+ magic = f.read(20)
503
+ if len(magic) < 20 or magic[:4] != b"\x7fELF":
504
+ return "unknown"
505
+ ei_class = magic[4]
506
+ e_machine = int.from_bytes(magic[18:20], "little")
507
+ bits = 64 if ei_class == 2 else 32
508
+ arch = {183: "arm64-v8a", 40: "armeabi-v7a", 62: "x86_64", 3: "x86"}.get(e_machine, "unknown")
509
+ return arch
510
+ except OSError:
511
+ return "unknown"
512
+
513
+
514
+ def main():
515
+ parser = argparse.ArgumentParser(description="Android 功能清单 → JNI → so/a 文件分析")
516
+ parser.add_argument("target", help="Android 工程目录")
517
+ parser.add_argument("--json", action="store_true")
518
+ parser.add_argument("--html", metavar="PATH", nargs="?", const="auto")
519
+ parser.add_argument("--output", metavar="DIR", help="输出目录(默认 <工程名>-native-reports/)")
520
+ args = parser.parse_args()
521
+
522
+ root = os.path.abspath(args.target)
523
+ if not os.path.isdir(root):
524
+ print("目标不存在: %s" % root)
525
+ return 2
526
+
527
+ # 校验能解析 ELF 的 nm 工具(so 符号提取的核心依赖,缺失直接报错)
528
+ nm_tool = find_elf_nm()
529
+ if not nm_tool:
530
+ print("错误:未找到能解析 ELF 的 llvm-nm / GNU nm 工具")
531
+ print("安装方法:")
532
+ print(" ① DevEco Studio(推荐):https://developer.huawei.com/consumer/cn/deveco-studio/")
533
+ print(" (装好后脚本自动探测 SDK,或设置 OHOS_SDK_HOME / DEVECO_SDK_HOME / ANDROID_NDK_HOME)")
534
+ print(" ② macOS:brew install llvm")
535
+ print(" ③ Linux:sudo apt install binutils")
536
+ return 2
537
+
538
+ # ① 扫描源码 + 原生库
539
+ sources = scan_sources(root)
540
+ project_libs = find_project_libs(root)
541
+ sources_info = []
542
+ for src in sources:
543
+ info = extract_file_info(src)
544
+ if info:
545
+ sources_info.append(info)
546
+
547
+ # ② 构建功能清单
548
+ cmake_deps = build_cmake_dep_map(root)
549
+ features = build_feature_manifest(sources_info, project_libs, nm_tool, cmake_deps, root)
550
+
551
+ # ③ 输出
552
+ out_dir = os.path.abspath(args.output) if args.output else os.path.join(
553
+ os.path.dirname(root), os.path.basename(root) + "-native-reports")
554
+ os.makedirs(out_dir, exist_ok=True)
555
+
556
+ # 收集所有 .so/.a,按文件名分组去重(同名多 ABI → 只保留 arm64-v8a,无 arm64 保留第一个)
557
+ _lib_files = sorted(set(
558
+ os.path.join(dp, fn) for dp, dns, fns in os.walk(root)
559
+ for fn in fns if fn.endswith((".so", ".a"))
560
+ if not any(d in dp for d in EXCLUDE_DIRS)
561
+ ))
562
+ _by_name = {}
563
+ for _p in _lib_files:
564
+ _by_name.setdefault(os.path.basename(_p), []).append(_p)
565
+ _deduped = []
566
+ for _name in sorted(_by_name):
567
+ _paths = _by_name[_name]
568
+ _arm64 = [p for p in _paths if "arm64-v8a" in p]
569
+ _chosen = _arm64[0] if _arm64 else _paths[0]
570
+ _deduped.append(_chosen)
571
+ all_libs = []
572
+ for p in _deduped:
573
+ arch = _get_lib_arch(p)
574
+ if arch == "unknown":
575
+ # .a 或读不到 ELF → 从目录名推断
576
+ parent = os.path.basename(os.path.dirname(p))
577
+ if parent in ("arm64-v8a", "armeabi-v7a", "x86", "x86_64", "armeabi"):
578
+ arch = parent
579
+ lib_info = {"path": p, "kind": "a" if p.endswith(".a") else "so", "arch": arch}
580
+ # 能力检测(从 so/a 导出符号,与 verify 共用同一套规则)
581
+ exports = nc.get_lib_exports(p, nm_tool)
582
+ lib_info["detected"] = nc.detect_capabilities(exports)
583
+ lib_info["export_count"] = len(exports)
584
+ all_libs.append(lib_info)
585
+
586
+ payload = {
587
+ "tool_version": __version__,
588
+ "target": root,
589
+ "features": features,
590
+ "all_libs": all_libs,
591
+ "summary": {
592
+ "total_features": len(features),
593
+ "total_jni": sum(len(f["jni_methods"]) for f in features),
594
+ "total_libs": len(set(l for f in features for l in f["involved_libs"])),
595
+
596
+ "libs": sorted(set(l for f in features for l in f["involved_libs"])),
597
+ },
598
+ }
599
+
600
+ # JSON(给 AI 映射用)
601
+ json_path = os.path.join(out_dir, "feature-manifest.json")
602
+ with open(json_path, "w", encoding="utf-8") as f:
603
+ json.dump(payload, f, ensure_ascii=False, indent=2)
604
+
605
+ # HTML(给人看)
606
+ html_path = os.path.join(out_dir, "feature-manifest.html")
607
+ if args.html != "no":
608
+ with open(html_path, "w", encoding="utf-8") as f:
609
+ f.write(render_html(payload))
610
+
611
+ print("=" * 72)
612
+ print("Android 功能清单 (tool %s)" % __version__)
613
+ print("目标: %s" % root)
614
+ print("功能: %d | JNI 方法: %d | 涉及原生库: %d" % (
615
+ payload["summary"]["total_features"], payload["summary"]["total_jni"],
616
+ payload["summary"]["total_libs"]))
617
+ print("JSON: %s" % json_path)
618
+ print("HTML: %s" % html_path)
619
+ print("=" * 72)
620
+ for f in features:
621
+ libs = ", ".join(f["involved_libs"])
622
+ print(" %s(%d JNI)→ %s" % (f["feature"], len(f["jni_methods"]), libs))
623
+
624
+ if args.json:
625
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
626
+ return 0
627
+
628
+
629
+ if __name__ == "__main__":
630
+ sys.exit(main())