@harmonyos-arkts/d2h 0.1.20 → 0.1.21

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 (151) 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-page-develop → d2h-spec-develop}/SKILL.md +143 -126
  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/agents/openai.yaml +0 -7
  138. package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
  139. package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
  140. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
  141. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
  142. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
  143. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
  144. package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
  145. package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
  146. package/skills/native-library-substitution/SKILL.md +0 -385
  147. package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
  148. package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
  149. package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
  150. package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
  151. package/skills/porting-cmake-native-libraries/references/report-and-build.md +0 -60
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env python3
2
+ """真机验证编排:准备 demo 工程 → 编译 → 签名 → 安装 → 运行 → 采集证据。
3
+
4
+ 让 verify 的静态能力判定得到真机运行证据支撑(14 个 Hypium 用例真实跑通)。
5
+
6
+ 流程:
7
+ ① 准备 demo 工程(复制模板 + 补 media 图标 + 生成测试 wav + 集成测试用例 + 替换待测 so)
8
+ ② ohpm install(装 @sj/ffmpeg 依赖)
9
+ ③ hvigorw 编译 app + ohosTest 两个 HAP(模板已带签名材料,自动签名)
10
+ ④ hdc install 两个 HAP
11
+ ⑤ aa test 运行 Hypium 用例
12
+ ⑥ hilog 采集 NATIVE_TEST 证据 → run.log
13
+
14
+ 用法:
15
+ python3 device-verify.py --demo <demo工程目录> --test-ets <FFmpegVerify.test.ets> \
16
+ --so <待测so> [--media-source <带media图标的工程>] [--device <序列号>] --output <run.log>
17
+ """
18
+ import argparse
19
+ import os
20
+ import re
21
+ import shutil
22
+ import subprocess
23
+ import sys
24
+ import wave
25
+ import struct
26
+ import math
27
+
28
+ HERE = os.path.dirname(os.path.abspath(__file__))
29
+ TEMPLATE = os.path.join(HERE, "..", "references", "demo-project-template")
30
+
31
+ TOOL_PATHS = {
32
+ "ohpm": ["/Applications/DevEco-Studio.app/Contents/tools/ohpm/bin/ohpm"],
33
+ "hvigorw": ["/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js"],
34
+ "hdc": ["/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc"],
35
+ }
36
+
37
+
38
+ def find_tool(name):
39
+ p = shutil.which(name)
40
+ if p:
41
+ return p
42
+ for c in TOOL_PATHS.get(name, []):
43
+ if os.path.isfile(c):
44
+ return c
45
+ return None
46
+
47
+
48
+ def run(cmd, cwd=None, shell=True, env=None):
49
+ return subprocess.run(cmd, shell=shell, cwd=cwd, stdout=subprocess.PIPE,
50
+ stderr=subprocess.STDOUT, text=True, errors="replace", env=env)
51
+
52
+
53
+ def gen_test_wav(path, seconds=2, rate=44100):
54
+ """生成 2 秒 440Hz 正弦波 wav(测试音频,供 FFAudioPlayer / 转码用例)。"""
55
+ os.makedirs(os.path.dirname(path), exist_ok=True)
56
+ with wave.open(path, "w") as w:
57
+ w.setnchannels(1)
58
+ w.setsampwidth(2)
59
+ w.setframerate(rate)
60
+ frames = bytearray()
61
+ for i in range(rate * seconds):
62
+ v = int(12000 * math.sin(2 * math.pi * 440 * i / rate))
63
+ frames += struct.pack("<h", v)
64
+ w.writeframes(bytes(frames))
65
+
66
+
67
+ def prepare_demo(demo_dir, test_ets, so_path, media_source):
68
+ """从模板复制 demo 工程,补 media 图标、wav、测试用例、待测 so。"""
69
+ if not os.path.isdir(TEMPLATE):
70
+ print("demo 模板不存在:", TEMPLATE)
71
+ return False
72
+ print(f"① 准备 demo 工程: {demo_dir}")
73
+ if os.path.exists(demo_dir):
74
+ shutil.rmtree(demo_dir)
75
+ shutil.copytree(TEMPLATE, demo_dir)
76
+
77
+ # 补 media 图标(从 media_source 工程复制)
78
+ if media_source and os.path.isdir(media_source):
79
+ for rel in [
80
+ "AppScope/resources/base/media",
81
+ "entry/src/main/resources/base/media",
82
+ ]:
83
+ src = os.path.join(media_source, rel)
84
+ dst = os.path.join(demo_dir, rel)
85
+ if os.path.isdir(src):
86
+ os.makedirs(dst, exist_ok=True)
87
+ for fn in os.listdir(src):
88
+ shutil.copy2(os.path.join(src, fn), os.path.join(dst, fn))
89
+ print(f" 补 media: {rel}")
90
+ # 修正 app.json5 的 icon 引用(模板引用 app_icon,但分层图标是 layered_image)
91
+ app_json = os.path.join(demo_dir, "AppScope", "app.json5")
92
+ if os.path.isfile(app_json):
93
+ s = open(app_json).read().replace("$media:app_icon", "$media:layered_image")
94
+ open(app_json, "w").write(s)
95
+
96
+ # 生成测试 wav
97
+ wav_path = os.path.join(demo_dir, "entry/src/main/resources/rawfile/ffmpeg_test.wav")
98
+ gen_test_wav(wav_path)
99
+ print(" 生成测试 wav: rawfile/ffmpeg_test.wav")
100
+
101
+ # 集成测试用例
102
+ dst_test = os.path.join(demo_dir, "entry/src/ohosTest/ets/test/FFmpegVerify.test.ets")
103
+ shutil.copy2(test_ets, dst_test)
104
+ print(" 集成测试用例: FFmpegVerify.test.ets")
105
+
106
+ # 自动回填签名材料(模板是占位符;从本机 ~/.ohos/config/ + 已签名工程取真实材料)
107
+ fill_signing_configs(demo_dir)
108
+
109
+ # 记录待测 so(编译后替换 oh_modules 里的 so)
110
+ demo_dir_meta = os.path.join(demo_dir, ".verify-so.txt")
111
+ with open(demo_dir_meta, "w") as f:
112
+ f.write(so_path)
113
+ return True
114
+
115
+
116
+ def fill_signing_configs(demo_dir):
117
+ """模板签名配置是占位符;从本机 DevEco 自动签名材料(~/.ohos/config/)+ 已签名工程的
118
+ build-profile.json5 回填真实路径与密文密码(机制同 references/demo-build-sign-run.md)。"""
119
+ bp = os.path.join(demo_dir, "build-profile.json5")
120
+ if not os.path.isfile(bp):
121
+ return
122
+ s = open(bp, encoding="utf-8").read()
123
+ if "<SIGNING_DIR>" not in s:
124
+ return # 已有真实材料(如 --demo 指向已配置好的工程)
125
+ cfg_dir = os.path.expanduser("~/.ohos/config")
126
+ if not os.path.isdir(cfg_dir):
127
+ print(" ⚠ 未找到 ~/.ohos/config 签名材料,真机验证可能编译失败(参考 references/demo-build-sign-run.md 手动配置)")
128
+ return
129
+ # 找与 bundleName 匹配的最新一套 .cer/.p12/.p7b(default_<Bundle>_<hash>.xxx)
130
+ app_json = os.path.join(demo_dir, "AppScope", "app.json5")
131
+ bundle = ""
132
+ if os.path.isfile(app_json):
133
+ m = re.search(r'"bundleName"\s*:\s*"([^"]+)"', open(app_json, encoding="utf-8").read())
134
+ if m:
135
+ bundle = m.group(1)
136
+ stem = bundle.split(".")[-1] if bundle else ""
137
+ candidates = []
138
+ for fn in os.listdir(cfg_dir):
139
+ if fn.endswith(".p7b") and (not stem or stem in fn):
140
+ p7b = os.path.join(cfg_dir, fn)
141
+ base = p7b[:-4]
142
+ if os.path.isfile(base + ".cer") and os.path.isfile(base + ".p12"):
143
+ candidates.append((os.path.getmtime(p7b), base))
144
+ if not candidates:
145
+ # 兜底:不带 bundle 过滤,取最新一套
146
+ for fn in os.listdir(cfg_dir):
147
+ if fn.endswith(".p7b"):
148
+ p7b = os.path.join(cfg_dir, fn)
149
+ base = p7b[:-4]
150
+ if os.path.isfile(base + ".cer") and os.path.isfile(base + ".p12"):
151
+ candidates.append((os.path.getmtime(p7b), base))
152
+ if not candidates:
153
+ print(" ⚠ ~/.ohos/config 无完整签名三件套(.cer/.p12/.p7b),真机验证可能编译失败")
154
+ return
155
+ _, base = max(candidates)
156
+ # 密文密码无法自造:从本机已签名工程的 build-profile.json5 里取同材料(hash 相同)的密文
157
+ pw = ""
158
+ stem_hash = os.path.basename(base).split("_", 2)[-1] if base.count("_") >= 2 else ""
159
+ for dp, dns, fns in os.walk(os.path.expanduser("~/Desktop")):
160
+ dns[:] = [d for d in dns if d not in ("oh_modules", "build", "node_modules", ".git")]
161
+ if os.path.basename(dp) != "":
162
+ for fn in fns:
163
+ if fn == "build-profile.json5" and stem_hash and stem_hash in open(os.path.join(dp, fn), encoding="utf-8", errors="replace").read(1 << 16):
164
+ mp = re.search(r"keyPassword:\s*'([0-9a-f]+)'", open(os.path.join(dp, fn), encoding="utf-8", errors="replace").read(1 << 16))
165
+ if mp:
166
+ pw = mp.group(1)
167
+ break
168
+ if pw:
169
+ break
170
+ if not pw:
171
+ print(" ⚠ 未能从本机已签名工程取回密文密码,真机验证可能编译失败(参考 demo-build-sign-run.md)")
172
+ return
173
+ s = (s.replace("<SIGNING_DIR>/<project>.cer", base + ".cer")
174
+ .replace("<SIGNING_DIR>/<project>.p7b", base + ".p7b")
175
+ .replace("<SIGNING_DIR>/<project>.p12", base + ".p12")
176
+ .replace("<KEY_PASSWORD>", pw)
177
+ .replace("<STORE_PASSWORD>", pw))
178
+ open(bp, "w", encoding="utf-8").write(s)
179
+ print(f" 回填签名材料: {os.path.basename(base)}")
180
+
181
+
182
+ def ohpm_install(demo_dir):
183
+ ohpm = find_tool("ohpm")
184
+ if not ohpm:
185
+ print("警告:未找到 ohpm,跳过依赖安装(假设已存在)")
186
+ return True
187
+ print("② ohpm install @sj/ffmpeg ...")
188
+ r = run(f'"{ohpm}" install', cwd=demo_dir)
189
+ if r.returncode != 0:
190
+ print(" ohpm install 输出:", r.stdout[-500:])
191
+ return r.returncode == 0
192
+
193
+
194
+ def replace_so(demo_dir, so_path):
195
+ """把待测 so 替换到 oh_modules 的 libffmpeg.so。"""
196
+ if not so_path or not os.path.isfile(so_path):
197
+ return
198
+ targets = []
199
+ for dp, dns, fns in os.walk(os.path.join(demo_dir, "oh_modules")):
200
+ for fn in fns:
201
+ if fn == "libffmpeg.so":
202
+ targets.append(os.path.join(dp, fn))
203
+ if targets:
204
+ for t in targets:
205
+ shutil.copy2(so_path, t)
206
+ print(f" 替换待测 so: {len(targets)} 处")
207
+
208
+
209
+ def build(demo_dir):
210
+ hvigorw = find_tool("hvigorw")
211
+ if not hvigorw:
212
+ print("错误:未找到 hvigorw(需安装 DevEco Studio)")
213
+ return False
214
+ node = shutil.which("node")
215
+ env = dict(os.environ)
216
+ env["DEVECO_SDK_HOME"] = env.get("DEVECO_SDK_HOME", "/Applications/DevEco-Studio.app/Contents/sdk")
217
+ # 先停残留 daemon(避免上次编译的 daemon cwd 已被删导致的 ENOENT)
218
+ run(f'{node} "{hvigorw}" --stop-daemon', cwd=demo_dir, env=env)
219
+ print("③ 编译 app HAP ...")
220
+ r1 = run(f'{node} "{hvigorw}" --mode module -p product=default assembleHap',
221
+ cwd=demo_dir, env=env)
222
+ if r1.returncode != 0:
223
+ print(" app 编译失败:", r1.stdout[-800:])
224
+ return False
225
+ print(" 编译 ohosTest HAP ...")
226
+ r2 = run(f'{node} "{hvigorw}" --mode module -p module=entry@ohosTest -p product=default assembleHap',
227
+ cwd=demo_dir, env=env)
228
+ if r2.returncode != 0:
229
+ print(" ohosTest 编译失败:", r2.stdout[-800:])
230
+ return False
231
+ return True
232
+
233
+
234
+ def install_and_run(demo_dir, device, bundle="com.example.protobufdemo", module="entry_test"):
235
+ hdc = find_tool("hdc")
236
+ if not hdc:
237
+ print("错误:未找到 hdc")
238
+ return False
239
+ if not device:
240
+ r = run(f'"{hdc}" list targets')
241
+ lines = [l.strip() for l in r.stdout.splitlines() if l.strip()]
242
+ if not lines:
243
+ print("错误:无连接设备")
244
+ return False
245
+ device = lines[0]
246
+ app_hap = os.path.join(demo_dir, "entry/build/default/outputs/default/entry-default-signed.hap")
247
+ test_hap = os.path.join(demo_dir, "entry/build/default/outputs/ohosTest/entry-ohosTest-signed.hap")
248
+ print(f"④ 安装 HAP 到设备 {device} ...")
249
+ run(f'"{hdc}" -t {device} install -r "{app_hap}"')
250
+ run(f'"{hdc}" -t {device} install -r "{test_hap}"')
251
+ print("⑤ 运行 Hypium 测试 ...")
252
+ r = run(f'"{hdc}" -t {device} shell aa test -b {bundle} -m {module} '
253
+ f'-s unittest OpenHarmonyTestRunner -s timeout 120000')
254
+ print(" 测试结果:", r.stdout.strip().splitlines()[-1] if r.stdout.strip() else "(空)")
255
+ return True
256
+
257
+
258
+ def collect(device, output):
259
+ hdc = find_tool("hdc")
260
+ if not hdc or not device:
261
+ return False
262
+ r = run(f'"{hdc}" -t {device} shell "hilog -x"')
263
+ lines = [l for l in r.stdout.splitlines()
264
+ if "NATIVE_TEST" in l or "OHOS_REPORT_RESULT" in l]
265
+ if lines:
266
+ os.makedirs(os.path.dirname(output), exist_ok=True)
267
+ with open(output, "w") as f:
268
+ f.write("\n".join(lines) + "\n")
269
+ print(f"⑥ 采集证据: {output}({len(lines)} 行)")
270
+ return True
271
+ print("警告:未采集到 NATIVE_TEST 日志")
272
+ return False
273
+
274
+
275
+ def main():
276
+ ap = argparse.ArgumentParser(description="真机验证编排")
277
+ ap.add_argument("--demo", required=True, help="demo 工程目录")
278
+ ap.add_argument("--test-ets", required=True, help="Hypium 测试用例文件")
279
+ ap.add_argument("--so", required=True, help="待测 so 路径")
280
+ ap.add_argument("--media-source", help="提供 media 图标的工程目录")
281
+ ap.add_argument("--device", help="设备序列号")
282
+ ap.add_argument("--output", required=True, help="证据输出 run.log")
283
+ args = ap.parse_args()
284
+
285
+ if not prepare_demo(args.demo, args.test_ets, args.so, args.media_source):
286
+ return 1
287
+ ohpm_install(args.demo)
288
+ replace_so(args.demo, args.so)
289
+ if not build(args.demo):
290
+ return 1
291
+ # 拿到设备序列号(install_and_run 内部可能解析)
292
+ hdc = find_tool("hdc")
293
+ device = args.device
294
+ if not device and hdc:
295
+ r = run(f'"{hdc}" list targets')
296
+ lines = [l.strip() for l in r.stdout.splitlines() if l.strip()]
297
+ device = lines[0] if lines else None
298
+ install_and_run(args.demo, device)
299
+ collect(device, args.output)
300
+ return 0
301
+
302
+
303
+ if __name__ == "__main__":
304
+ sys.exit(main())
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env python3
2
+ """d2h-native-library 两个 skill 共享的工具/能力检测模块。
3
+
4
+ 供 d2h-native-library-analyse 和 d2h-native-library-verify 复用:
5
+ - 工具发现(llvm-nm / llvm-readelf / c++filt,自动探测 DevEco SDK / Android NDK 路径)
6
+ - 场景分类(classify_feature,Java 类名 → 功能场景)
7
+ - 能力检测(CAPABILITY_RULES:从 so/a 导出符号检测真实能力)
8
+ - 符号 demangle(C++ mangled 名 → 可读的 FFAV::FFAudioPlayer::Play(...))
9
+ """
10
+ import html as _html
11
+ import os
12
+ import re
13
+ import shutil
14
+ import subprocess
15
+
16
+ # ==================== 工具发现 ====================
17
+ TOOL_INSTALL_HINTS = {
18
+ "llvm-nm": "① DevEco Studio(推荐)https://developer.huawei.com/consumer/cn/deveco-studio/ "
19
+ "② macOS: brew install llvm ③ Linux: sudo apt install binutils",
20
+ "llvm-readelf": "① DevEco Studio(推荐)https://developer.huawei.com/consumer/cn/deveco-studio/ "
21
+ "② macOS: brew install llvm ③ Linux: sudo apt install binutils",
22
+ "c++filt": "① DevEco Studio(自带 llvm-cxxfilt) ② macOS: 系统自带 /usr/bin/c++filt ③ Linux: sudo apt install binutils",
23
+ }
24
+
25
+
26
+ def run(cmd):
27
+ try:
28
+ return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
29
+ text=True, errors="replace").stdout
30
+ except OSError:
31
+ return ""
32
+
33
+
34
+ def sdk_bin_dirs():
35
+ """返回可能包含 llvm-nm/llvm-readelf/c++filt 的 SDK bin 目录列表。"""
36
+ dirs = []
37
+ home = os.path.expanduser("~")
38
+
39
+ def add(d):
40
+ if d and os.path.isdir(d):
41
+ dirs.append(d)
42
+
43
+ # Android NDK
44
+ for env in ("ANDROID_NDK_HOME", "ANDROID_NDK_ROOT"):
45
+ v = os.environ.get(env)
46
+ if v:
47
+ add(os.path.join(v, "toolchains/llvm/prebuilt"))
48
+ sdk = os.environ.get("ANDROID_HOME")
49
+ if sdk:
50
+ ndk = os.path.join(sdk, "ndk")
51
+ if os.path.isdir(ndk):
52
+ for v in os.listdir(ndk):
53
+ add(os.path.join(ndk, v, "toolchains/llvm/prebuilt"))
54
+ # DevEco / OpenHarmony SDK
55
+ for env in ("OHOS_SDK_HOME", "DEVECO_SDK_HOME"):
56
+ v = os.environ.get(env)
57
+ if v:
58
+ for sub in ("default/openharmony/native/llvm/bin",
59
+ "openharmony/native/llvm/bin", "native/llvm/bin"):
60
+ add(os.path.join(v, sub))
61
+ add("/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native/llvm/bin")
62
+ add("/Applications/DevEco-Studio.app/Contents/sdk/default/hms/native/BiSheng/bin")
63
+ add(os.path.join(home, "Library/OpenHarmony/Sdk"))
64
+ return dirs
65
+
66
+
67
+ def resolve_tool(names):
68
+ for n in names:
69
+ p = shutil.which(n)
70
+ if p:
71
+ return p
72
+ for d in sdk_bin_dirs():
73
+ for n in names:
74
+ c = os.path.join(d, n)
75
+ if os.path.isfile(c):
76
+ return c
77
+ return None
78
+
79
+
80
+ def _is_elf_nm(nm_path):
81
+ """判断 nm 能否解析 ELF:llvm-nm 可以;GNU nm 可以;macOS 旧版 /usr/bin/nm(Mach-O)不行。"""
82
+ if not nm_path:
83
+ return False
84
+ if "llvm-nm" in os.path.basename(nm_path):
85
+ return True
86
+ try:
87
+ out = subprocess.run([nm_path, "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
88
+ text=True, errors="replace").stdout
89
+ return "GNU" in out or "llvm-nm" in out
90
+ except Exception:
91
+ return False
92
+
93
+
94
+ def find_elf_nm():
95
+ """找一个能解析 ELF 的 nm 工具(llvm-nm 优先,GNU nm 次之)。找不到返回 None。"""
96
+ p = shutil.which("llvm-nm")
97
+ if p:
98
+ return p
99
+ for d in sdk_bin_dirs():
100
+ c = os.path.join(d, "llvm-nm")
101
+ if os.path.isfile(c):
102
+ return c
103
+ p = shutil.which("nm")
104
+ if p and _is_elf_nm(p):
105
+ return p
106
+ return None
107
+
108
+
109
+ def find_llvm_readelf():
110
+ p = shutil.which("llvm-readelf")
111
+ if p:
112
+ return p
113
+ for d in sdk_bin_dirs():
114
+ c = os.path.join(d, "llvm-readelf")
115
+ if os.path.isfile(c):
116
+ return c
117
+ p = shutil.which("readelf")
118
+ if p:
119
+ return p
120
+ return None
121
+
122
+
123
+ def find_cxxfilt():
124
+ """找一个 demangle 工具(c++filt 优先,llvm-cxxfilt 次之)。找不到返回 None。"""
125
+ for n in ("c++filt", "llvm-cxxfilt"):
126
+ p = shutil.which(n)
127
+ if p:
128
+ return p
129
+ for d in sdk_bin_dirs():
130
+ for n in ("c++filt", "llvm-cxxfilt"):
131
+ c = os.path.join(d, n)
132
+ if os.path.isfile(c):
133
+ return c
134
+ return None
135
+
136
+
137
+ def find_hdc():
138
+ """找 hdc(鸿蒙设备命令行工具,DevEco SDK 自带)。找不到返回 None。"""
139
+ p = shutil.which("hdc")
140
+ if p:
141
+ return p
142
+ for d in sdk_bin_dirs():
143
+ c = os.path.join(d, "hdc")
144
+ if os.path.isfile(c):
145
+ return c
146
+ for c in ["/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc",
147
+ "/Applications/DevEco-Studio.app/Contents/sdk/default/hms/toolchains/hdc"]:
148
+ if os.path.isfile(c):
149
+ return c
150
+ return None
151
+
152
+
153
+ def list_devices(hdc=None):
154
+ """返回已连接的设备序列号列表。无设备返回 []。"""
155
+ hdc = hdc or find_hdc()
156
+ if not hdc:
157
+ return []
158
+ out = run([hdc, "list", "targets"])
159
+ return [l.strip() for l in out.splitlines() if l.strip()]
160
+
161
+
162
+ # ==================== 符号 demangle ====================
163
+ def demangle_symbol(sym, cxxfilt=None):
164
+ """把 C++ mangled 符号 demangle 成可读名字。失败返回原符号。"""
165
+ if not cxxfilt or not sym or not sym.startswith("_Z"):
166
+ return sym
167
+ try:
168
+ out = subprocess.run([cxxfilt, sym], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
169
+ text=True, errors="replace").stdout
170
+ d = out.strip()
171
+ return d if d and d != sym else sym
172
+ except Exception:
173
+ return sym
174
+
175
+
176
+ # ==================== 场景分类 ====================
177
+ def classify_feature(class_name):
178
+ """按 Java 类名精确匹配功能场景(与 analyse/verify 共用,避免 Player 子串误伤)。"""
179
+ cls = (class_name or "").lower()
180
+ if "nextplayer" in cls:
181
+ return "视频播放(NextPlayer引擎)"
182
+ if "videoplayer" in cls:
183
+ return "视频滤镜预览(VideoPlayer)"
184
+ if "audioplayer" in cls or "musicplayer" in cls:
185
+ return "音频播放(AudioPlayer)"
186
+ if "ffmpegcmd" in cls or "transcode" in cls:
187
+ return "视频剪辑处理(FFmpegCmd)"
188
+ if "mediaretriever" in cls or "metadata" in cls:
189
+ return "媒体格式探测(MediaRetriever)"
190
+ if "livepusher" in cls or "rtmp" in cls:
191
+ return "直播推流(LivePusher)"
192
+ if "visualizer" in cls or "fft" in cls or "spectrum" in cls:
193
+ return "音频可视化(FrankVisualizer)"
194
+ if "equalizer" in cls:
195
+ return "均衡器(Equalizer)"
196
+ return None
197
+
198
+
199
+ # ==================== so/a 符号提取 ====================
200
+ def get_lib_exports(so_path, nm_tool):
201
+ """提取 .so/.a 的对外符号名集合。
202
+
203
+ - .so:动态导出符号(llvm-nm -D --defined-only --extern-only)
204
+ - .a :全局定义符号(llvm-nm --defined-only;ar 归档无动态符号表,
205
+ 取类型为大写字母 T/D/B/R/C 的全局符号)
206
+ """
207
+ if not nm_tool:
208
+ return set()
209
+ if so_path.endswith(".a"):
210
+ out = run([nm_tool, "--defined-only", so_path])
211
+ exports = set()
212
+ for line in out.splitlines():
213
+ s = line.strip()
214
+ if not s or s.endswith(":"):
215
+ continue
216
+ parts = s.split()
217
+ if len(parts) >= 3 and parts[1] and parts[1][0].isupper():
218
+ exports.add(parts[2])
219
+ return exports
220
+ out = run([nm_tool, "-D", "--defined-only", "--extern-only", so_path])
221
+ exports = set()
222
+ for line in out.splitlines():
223
+ parts = line.split()
224
+ if len(parts) >= 3:
225
+ exports.add(parts[2])
226
+ return exports
227
+
228
+
229
+ # ==================== 能力检测(从 so/a 导出符号判断)====================
230
+ # 每项:(能力名, 符号名正则, 说明)。正则匹配导出符号名,命中任一即证明 so/a 真实具备该能力。
231
+ CAPABILITY_RULES = [
232
+ # FFmpeg 核心库
233
+ ("音视频编解码", r"^avcodec_", "avcodec 库(编解码核心)"),
234
+ ("封装/解封装", r"^avformat_", "avformat 库(封装/解封装)"),
235
+ ("滤镜", r"^avfilter_", "avfilter 库(滤镜)"),
236
+ ("视频缩放", r"^swscale_", "swscale 库(像素缩放)"),
237
+ ("音频重采样", r"^swresample_", "swresample 库(音频重采样)"),
238
+ ("IO", r"^avio_", "avio 库(文件/网络 IO)"),
239
+ # 编码器
240
+ ("H.264 编码", r"^ff_libx264_encoder$|^x264_encoder_", "H.264 编码器(so 的 ff_libx264_encoder / .a 的 x264_encoder_*)"),
241
+ ("AAC 编码", r"^ff_aac_encoder$|^faacEnc", "AAC 编码器(so 的 ff_aac_encoder / .a 的 faacEnc*)"),
242
+ ("AV1 编解码", r"^ff_libaom_av1_", "libaom AV1 编解码器"),
243
+ # 协议与封装
244
+ ("RTMP 协议", r"^ff_rtmp_protocol$", "RTMP 推拉流协议"),
245
+ ("RTSP 协议", r"^ff_rtsp_protocol$", "RTSP 协议"),
246
+ ("HTTP(S) 协议", r"^ff_https?_protocol$", "HTTP/HTTPS 协议"),
247
+ ("FLV 封装", r"^ff_flv_muxer$", "FLV 封装器"),
248
+ ("MP4 封装", r"^ff_mp4_muxer$", "MP4 封装器"),
249
+ ("MPEG-TS 封装", r"^ff_mpegts_muxer$", "MPEG-TS 封装器"),
250
+ ("HLS 封装", r"^ff_hls_muxer$", "HLS 封装器"),
251
+ # NAPI 层(@sj/ffmpeg 的 ArkTS 接口)
252
+ ("音频播放 NAPI", r"FFAudioPlayer", "FFAudioPlayer NAPI 类(播放/时长/位置/seek)"),
253
+ ("音频写入 NAPI", r"FFAudioWriter", "FFAudioWriter NAPI 类(PCM 编码写入)"),
254
+ ("任务取消 NAPI", r"FFAbortSignal", "FFAbortController/FFAbortSignal NAPI 类"),
255
+ ("命令执行 NAPI", r"FFmpeg", "FFmpeg NAPI(执行 ffmpeg/ffprobe 命令)"),
256
+ # 常见非 FFmpeg 原生库(换一个 so 也能识别)
257
+ ("AES 加密", r"^AES_", "OpenSSL AES 加解密"),
258
+ ("RSA 非对称", r"^RSA_", "OpenSSL RSA"),
259
+ ("X509 证书", r"^X509_", "OpenSSL X509 证书"),
260
+ ("TLS/SSL", r"^SSL_|^TLS_", "OpenSSL TLS/SSL"),
261
+ ("EVP 加密接口", r"^EVP_", "OpenSSL EVP 高层接口"),
262
+ ("SHA 摘要", r"^SHA(1|224|256|384|512)_", "OpenSSL SHA 系列"),
263
+ ("OpenSSL BN", r"^BN_", "OpenSSL BN 大数运算"),
264
+ ("OpenSSL CRYPTO", r"^CRYPTO_", "OpenSSL CRYPTO"),
265
+ ("OpenSSL 3.x(ossl)", r"^ossl_", "OpenSSL 3.x 内部符号"),
266
+ ("AMR 编解码", r"^AMREncode|^AMRDecode", "AMR 音频编解码"),
267
+ ("MP3 编码(LAME)", r"^lame_", "LAME MP3 编码器"),
268
+ ("AV1 编码(libaom)", r"^aom_", "libaom AV1 编码器"),
269
+ ("zlib 压缩", r"^(deflate|inflate|compress|uncompress)$", "zlib 压缩"),
270
+ ("HTTP 客户端", r"^curl_", "libcurl HTTP 客户端"),
271
+ ]
272
+
273
+
274
+ def detect_capabilities(exports):
275
+ """从 so/a 导出符号检测能力。返回 {能力名: 命中符号样例}(确定性:sorted 遍历)。"""
276
+ detected = {}
277
+ sorted_exports = sorted(exports)
278
+ for name, pattern, desc in CAPABILITY_RULES:
279
+ rx = re.compile(pattern)
280
+ for s in sorted_exports:
281
+ if rx.search(s):
282
+ detected[name] = s
283
+ break
284
+ return detected
285
+
286
+
287
+ def cluster_unknown_symbols(exports, top=25):
288
+ """对未匹配任何能力规则的符号,按前缀聚类,返回 [(前缀, 数量)]。
289
+
290
+ 用于自动发现 so 里未纳入规则的库(如 aom_/ossl_/lame_/BN_ 等),
291
+ 让未知 so 也有能力线索,不依赖硬编码规则。
292
+ """
293
+ matched = set()
294
+ for name, pattern, desc in CAPABILITY_RULES:
295
+ rx = re.compile(pattern)
296
+ for s in exports:
297
+ if rx.search(s):
298
+ matched.add(s)
299
+ unmatched = exports - matched
300
+ from collections import Counter
301
+ prefixes = Counter()
302
+ for s in unmatched:
303
+ m = re.match(r"^([A-Za-z_]+)_", s)
304
+ if m:
305
+ prefixes[m.group(1)] += 1
306
+ return prefixes.most_common(top)
307
+
308
+
309
+ # 符号前缀 → 语义(让聚类结果可读;有序匹配,长前缀优先)
310
+ PREFIX_MEANING = [
311
+ ("ff_vf", "FFmpeg 视频滤镜"),
312
+ ("ff_af", "FFmpeg 音频滤镜"),
313
+ ("ff_put", "FFmpeg 像素填充"),
314
+ ("ff_pcm", "FFmpeg PCM 格式"),
315
+ ("ff_avg", "FFmpeg 像素平均"),
316
+ ("ff_tx_tab", "FFmpeg 变换表"),
317
+ ("ff", "FFmpeg 内部符号"),
318
+ ("cfl", "libaom 帧内预测"),
319
+ ("aom", "libaom AV1 编码器"),
320
+ ("av", "FFmpeg avutil"),
321
+ ("ossl", "OpenSSL 3.x"),
322
+ ("OPENSSL", "OpenSSL 3.x"),
323
+ ("BN", "OpenSSL BN 大数"),
324
+ ("CRYPTO", "OpenSSL CRYPTO"),
325
+ ("BIO", "OpenSSL BIO"),
326
+ ("PEM", "OpenSSL PEM"),
327
+ ("lame", "LAME MP3"),
328
+ ("__emutls", "线程本地存储"),
329
+ ]
330
+
331
+
332
+ def prefix_meaning(prefix):
333
+ for key, meaning in PREFIX_MEANING:
334
+ if prefix.startswith(key):
335
+ return meaning
336
+ return ""
337
+
338
+
339
+ # ==================== HTML 工具 ====================
340
+ def esc(v):
341
+ return _html.escape(str(v if v is not None else "-"))