android2harmony 0.1.4 → 0.1.6
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.
- package/README.md +1 -407
- package/agents/self-tester.md +55 -376
- package/dist/index.js +238 -124
- package/dist/index.js.map +4 -4
- package/package.json +36 -32
- package/skills/a2h-resource-convert/SKILL.md +931 -0
- package/skills/a2h-resource-convert/references/code-vector-icon-rules.md +335 -0
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/conversion-rules.md +15 -2
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/dependency-analysis-rules.md +24 -2
- package/skills/a2h-resource-convert/references/lottie-conversion-rules.md +219 -0
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/resource-mapping-rules.md +27 -2
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/xml-drawable-to-svg-rules.md +118 -1
- package/skills/a2h-resource-convert/scripts/a2h_resource_convert.js +2186 -0
- package/skills/a2h-resource-convert/scripts/app_identity.js +741 -0
- package/skills/a2h-resource-convert/scripts/code_vector_icons.js +607 -0
- package/skills/a2h-resource-convert/scripts/package.json +3 -0
- package/skills/a2h-resource-convert/scripts/svg_fidelity_check.js +632 -0
- package/skills/a2h-ui-transfer/SKILL.md +431 -0
- package/skills/a2h-ui-transfer/references/conversion-procedure.md +547 -0
- package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/_/350/214/203/345/274/217/351/200/211/346/213/251/350/257/264/346/230/216.md +1 -1
- package/skills/a2h-ui-transfer/references/mvvm-v2/_/350/243/205/351/245/260/345/231/250/351/200/237/346/237/245.md +133 -0
- package/skills/{hmos-batch-ui-align/scripts/android_parse_fast.ts → a2h-ui-transfer/scripts/android_parse_fast.js} +532 -301
- package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +1624 -0
- package/skills/a2h-ui-transfer/scripts/measure_pack.js +1005 -0
- package/skills/a2h-ui-transfer/scripts/package.json +3 -0
- package/skills/hmos-fix-build-errors/SKILL.md +1 -1
- package/skills/hmos-incremental-ui-align/README.md +15 -15
- package/skills/hmos-incremental-ui-align/SKILL.md +15 -15
- package/skills/hmos-incremental-ui-align/references/State_Model_Template.md +2 -2
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.js +790 -0
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.js +285 -0
- package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +76 -76
- package/skills/hmos-incremental-ui-align/scripts/page_capture.js +756 -0
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.js +155 -0
- package/skills/hmos-integration-test/README.md +341 -0
- package/skills/hmos-integration-test/SKILL.md +446 -0
- package/skills/hmos-integration-test/scripts/report-tool.mjs +646 -0
- package/skills/hmos-integration-test/scripts/resolve-metadata-tool.mjs +147 -0
- package/skills/hmos-integration-test/scripts/self-test-runner.mjs +1006 -0
- package/skills/hmos-integration-test/scripts/testcases-tool.mjs +189 -0
- package/skills/hmos-spec-generate/SKILL.md +26 -24
- package/skills/hmos-spec-generate/scripts/parse_requirements.ts +515 -0
- package/skills/hmos-spec-generate/template/REQ.txt +22 -0
- package/skills/hmos-spec-generate/template/REQ.xlsx +0 -0
- package/skills/hmos-batch-ui-align/SKILL.md +0 -141
- package/skills/hmos-batch-ui-align/references/conversion-procedure.md +0 -217
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.ts +0 -999
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.ts +0 -343
- package/skills/hmos-incremental-ui-align/scripts/page_capture.ts +0 -977
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.ts +0 -188
- package/skills/hmos-resources-convert/SKILL.md +0 -654
- package/skills/hmos-resources-convert/template/AppScope/app.json5 +0 -10
- package/skills/hmos-resources-convert/template/AppScope/resources/base/element/string.json +0 -8
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/layered_image.json +0 -7
- package/skills/hmos-resources-convert/template/build-profile.json5 +0 -42
- package/skills/hmos-resources-convert/template/code-linter.json5 +0 -32
- package/skills/hmos-resources-convert/template/entry/build-profile.json5 +0 -33
- package/skills/hmos-resources-convert/template/entry/hvigorfile.ts +0 -6
- package/skills/hmos-resources-convert/template/entry/obfuscation-rules.txt +0 -23
- package/skills/hmos-resources-convert/template/entry/oh-package.json5 +0 -10
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entryability/EntryAbility.ets +0 -48
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +0 -16
- package/skills/hmos-resources-convert/template/entry/src/main/ets/pages/Index.ets +0 -23
- package/skills/hmos-resources-convert/template/entry/src/main/module.json5 +0 -55
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/color.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/float.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/string.json +0 -16
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/layered_image.json +0 -7
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/backup_config.json +0 -3
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/main_pages.json +0 -5
- package/skills/hmos-resources-convert/template/entry/src/main/resources/dark/element/color.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/mock/mock-config.json5 +0 -2
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/Ability.test.ets +0 -35
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/List.test.ets +0 -5
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/module.json5 +0 -16
- package/skills/hmos-resources-convert/template/entry/src/test/List.test.ets +0 -5
- package/skills/hmos-resources-convert/template/entry/src/test/LocalUnit.test.ets +0 -33
- package/skills/hmos-resources-convert/template/hvigor/hvigor-config.json5 +0 -23
- package/skills/hmos-resources-convert/template/hvigorfile.ts +0 -6
- package/skills/hmos-resources-convert/template/oh-package-lock.json5 +0 -28
- package/skills/hmos-resources-convert/template/oh-package.json5 +0 -10
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-interaction-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-layout-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Computed/350/243/205/351/245/260/345/231/250/357/274/232/350/256/241/347/256/227/345/261/236/346/200/247.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Event/350/243/205/351/245/260/345/231/250/357/274/232/350/247/204/350/214/203/347/273/204/344/273/266/350/276/223/345/207/272.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Local/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/351/203/250/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Monitor/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/344/277/256/346/224/271/345/274/202/346/255/245/347/233/221/345/220/254.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@ObservedV2/350/243/205/351/245/260/345/231/250/345/222/214@Trace/350/243/205/351/245/260/345/231/250/357/274/232/347/261/273/345/261/236/346/200/247/345/217/230/345/214/226/350/247/202/346/265/213.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Once/350/243/205/351/245/260/345/231/250/357/274/232/345/210/235/345/247/213/345/214/226/345/220/214/346/255/245/344/270/200/346/254/241.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Param/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Provider/350/243/205/351/245/260/345/231/250/345/222/214@Consumer/350/243/205/351/245/260/345/231/250/357/274/232/350/267/250/347/273/204/344/273/266/345/261/202/347/272/247/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Type/350/243/205/351/245/260/345/231/250/357/274/232/346/240/207/350/256/260/347/261/273/345/261/236/346/200/247/347/232/204/347/261/273/345/236/213.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/AppStorageV2/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/PersistenceV2/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/220/221V2/350/277/201/347/247/273/344/270/216/346/267/267/347/224/250/346/214/207/345/257/274.md" +0 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 代码内定义矢量图标提取与合成工具
|
|
4
|
+
*
|
|
5
|
+
* Jetpack Compose 应用的图标来自 `androidx.compose.material:material-icons-*`,
|
|
6
|
+
* 是 Kotlin 代码生成的 `ImageVector`,编译进 `classes*.dex`,**不进** `res/drawable`,
|
|
7
|
+
* 也不进 `resources.arsc`。因此本 skill 其余所有基于 `res/` 的步骤都看不见它们:
|
|
8
|
+
* 转换报告显示「依赖全部满足」,而迁移后的页面一个图标都不显示。
|
|
9
|
+
*
|
|
10
|
+
* 本工具做三件事:
|
|
11
|
+
* 1. 从 `classes*.dex` 字符串池提取图标引用清单(权威范围,含库内部使用)
|
|
12
|
+
* 2. 从 Android 源码 `.kt` 提取归属(哪个页面用哪个图标)——dex 无法提供
|
|
13
|
+
* 3. 按确定性规则解析到上游 Material Icons 图形,写出 HarmonyOS `base/media/*.svg`
|
|
14
|
+
*
|
|
15
|
+
* 规则依据见 ../references/code-vector-icon-rules.md。
|
|
16
|
+
*
|
|
17
|
+
* 零第三方依赖,仅使用 Node 内置模块,可独立分发。
|
|
18
|
+
* 纯 JavaScript(ESM),直接 `node code_vector_icons.js` 运行,无需编译、无依赖。
|
|
19
|
+
*
|
|
20
|
+
* 用法:
|
|
21
|
+
* node code_vector_icons.js --dex <decoded apk dir> --out <base/media dir>
|
|
22
|
+
* [--android-src <android_project_dir>]
|
|
23
|
+
* (--fetch | --snapshot <local icons dir>)
|
|
24
|
+
* [--ref <commit>] [--json <report path>]
|
|
25
|
+
* [--attributed-only] [--dry-run] [--quiet]
|
|
26
|
+
*
|
|
27
|
+
* --attributed-only: 只为源码中真正引用到的图标产出图形。用于图标库未被 R8 裁剪、
|
|
28
|
+
* 整库被打进 APK 的工程(工具会在检测到时给出提示)。
|
|
29
|
+
*
|
|
30
|
+
* 退出码:
|
|
31
|
+
* 0 全部图标解析成功(或无代码内图标)
|
|
32
|
+
* 1 存在无法解析的图标(必须记入报告的 unmappable 行)
|
|
33
|
+
* 2 用法错误 / 路径不存在 / 图形来源不可用
|
|
34
|
+
*
|
|
35
|
+
* 注意:--attributed-only 主动跳过的图标不算失败,不影响退出码。
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import * as fs from 'node:fs';
|
|
39
|
+
import * as path from 'node:path';
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────
|
|
42
|
+
// 常量
|
|
43
|
+
// ─────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/** 上游 google/material-design-icons 固定 commit。移动分支不可复现,必须 pin。 */
|
|
46
|
+
const DEFAULT_REF = '50f0603134ce7b70b2d71b686cc13e8b57ccb74c';
|
|
47
|
+
|
|
48
|
+
const RAW_BASE = 'https://raw.githubusercontent.com/google/material-design-icons/';
|
|
49
|
+
|
|
50
|
+
/** Compose 风格 → 上游目录名 / 产物文件名后缀 */
|
|
51
|
+
const STYLE = {
|
|
52
|
+
Filled: { dir: 'materialicons', suffix: 'filled' },
|
|
53
|
+
Outlined: { dir: 'materialiconsoutlined', suffix: 'outlined' },
|
|
54
|
+
Rounded: { dir: 'materialiconsround', suffix: 'round' },
|
|
55
|
+
Sharp: { dir: 'materialiconssharp', suffix: 'sharp' },
|
|
56
|
+
TwoTone: { dir: 'materialiconstwotone', suffix: 'twotone' },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* dex 字符串池中的图标名。`Icons.Default` 在 dex 中总是记为 `Filled.*`。
|
|
61
|
+
*
|
|
62
|
+
* 名字必须以大写字母或 `_` 开头:Compose 图标名是 PascalCase,数字开头的加 `_` 前缀。
|
|
63
|
+
* 这条约束用于排除 `Outlined.kt` 这类误命中——dex 里存在 Kotlin 源文件名字符串,
|
|
64
|
+
* 形如 `<Style>.kt`,正好套进图标名的形状。在 Tasks 上实测到 3 例。
|
|
65
|
+
*/
|
|
66
|
+
const DEX_ICON_RE = /(AutoMirrored\.)?(Filled|Outlined|Rounded|Sharp|TwoTone)\.[A-Z_][A-Za-z0-9_]*/g;
|
|
67
|
+
|
|
68
|
+
/** Kotlin 源码中的图标引用。源码侧才会出现 `Default`,需归一到 `Filled`。 */
|
|
69
|
+
const SRC_ICON_RE = /Icons\.(AutoMirrored\.)?(Filled|Outlined|Rounded|Sharp|TwoTone|Default)\.([A-Z_][A-Za-z0-9_]*)/g;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 上游改过名的图标:Compose 侧沿用旧名,`src/` 下已经是新名。
|
|
73
|
+
* 只收录实测确认的条目,不猜。
|
|
74
|
+
*/
|
|
75
|
+
const ALIASES = {
|
|
76
|
+
motorcycle: 'two_wheeler',
|
|
77
|
+
leave_bags_at_home: 'no_luggage',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** 判定「图标库未被 R8 裁剪」的阈值:总量大且绝大多数无源码引用。 */
|
|
81
|
+
const UNSHRUNK_MIN_REFS = 300;
|
|
82
|
+
const UNSHRUNK_UNKNOWN_RATIO = 0.8;
|
|
83
|
+
|
|
84
|
+
/** 上游 SVG 开头的透明占位边界框,无视觉信息,去掉。两种写法都要覆盖。 */
|
|
85
|
+
const GUARD_PATH_RE = /<path\s+d="M0 0h24v24H0(?:V0)?z"\s+fill="none"\s*\/>/g;
|
|
86
|
+
|
|
87
|
+
const SKIP_DIRS = new Set(['.git', 'build', '.gradle', 'node_modules', '.idea', 'out']);
|
|
88
|
+
|
|
89
|
+
// ─────────────────────────────────────────────
|
|
90
|
+
// 类型
|
|
91
|
+
// ─────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
// interface IconRef {
|
|
94
|
+
// /** dex/源码中的原始形式,如 `AutoMirrored.Outlined.Label` */
|
|
95
|
+
// compose: string;
|
|
96
|
+
// style: string;
|
|
97
|
+
// pascal: string;
|
|
98
|
+
// autoMirrored: boolean;
|
|
99
|
+
// /** material snake_case 名,如 `label` */
|
|
100
|
+
// material: string;
|
|
101
|
+
// category: string | null;
|
|
102
|
+
// /** 同名多 category 时的备选,需写入 notes */
|
|
103
|
+
// otherCategories: string[];
|
|
104
|
+
// upstream: string | null;
|
|
105
|
+
// media: string | null;
|
|
106
|
+
// screens: string[];
|
|
107
|
+
// suggestedPage: string | null;
|
|
108
|
+
// sourceRefs: string[];
|
|
109
|
+
// resolved: boolean;
|
|
110
|
+
// /** 仅在 resolved=false 时填写:为什么没有产出图形 */
|
|
111
|
+
// reason: string;
|
|
112
|
+
// /** 必须写进 mapping notes 的事实:改名、多 category、RTL 镜像 */
|
|
113
|
+
// notes: string[];
|
|
114
|
+
// bytes?: number;
|
|
115
|
+
// }
|
|
116
|
+
|
|
117
|
+
// interface Report {
|
|
118
|
+
// ref: string;
|
|
119
|
+
// artworkSource: string;
|
|
120
|
+
// dexDir: string;
|
|
121
|
+
// androidSrc: string | null;
|
|
122
|
+
// outDir: string;
|
|
123
|
+
// /** dex 清单远大于源码引用 → 图标库很可能未被 R8 裁剪,整库被打进包 */
|
|
124
|
+
// libraryLikelyUnshrunk: boolean;
|
|
125
|
+
// attributedOnly: boolean;
|
|
126
|
+
// counts: {
|
|
127
|
+
// referencesFound: number;
|
|
128
|
+
// resolved: number;
|
|
129
|
+
// unresolved: number;
|
|
130
|
+
// mediaFilesWritten: number;
|
|
131
|
+
// autoMirrored: number;
|
|
132
|
+
// screenAttributed: number;
|
|
133
|
+
// screenUnknown: number;
|
|
134
|
+
// };
|
|
135
|
+
// icons: IconRef[];
|
|
136
|
+
// }
|
|
137
|
+
|
|
138
|
+
// ─────────────────────────────────────────────
|
|
139
|
+
// 工具函数
|
|
140
|
+
// ─────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function fail(msg) {
|
|
143
|
+
console.error(`[code_vector_icons] ${msg}`);
|
|
144
|
+
process.exit(2);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* PascalCase → 候选 snake_case(只按大小写边界断词,不碰数字)。
|
|
149
|
+
*
|
|
150
|
+
* 数字周围的断词在上游没有统一规则:`Filter1`→`filter_1` 但 `Co2`→`co2`,
|
|
151
|
+
* `Rotate90DegreesCcw`→`rotate_90_degrees_ccw` 但 `StarPurple500`→`star_purple500`,
|
|
152
|
+
* `Crop169`→`crop_16_9`。任何固定的数字断词规则都会错一批,所以数字交给
|
|
153
|
+
* 「去下划线后比对」的索引查找去解决,见 resolveName()。
|
|
154
|
+
*/
|
|
155
|
+
function caseTransform(pascal) {
|
|
156
|
+
let n = pascal.replace(/^_+/, ''); // Compose 给数字开头的名字加 `_` 前缀:_360 → 360
|
|
157
|
+
n = n.replace(/(?<=[a-z0-9])(?=[A-Z])/g, '_');
|
|
158
|
+
n = n.replace(/(?<=[A-Z])(?=[A-Z][a-z])/g, '_');
|
|
159
|
+
return n.toLowerCase();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 索引:既有精确名,也有「去下划线」名,后者吸收全部数字断词差异。 */
|
|
163
|
+
// interface IconIndex {
|
|
164
|
+
// exact: Map<string, string[]>;
|
|
165
|
+
// squashed: Map<string, string[]>;
|
|
166
|
+
// }
|
|
167
|
+
|
|
168
|
+
// interface Resolution {
|
|
169
|
+
// material: string | null;
|
|
170
|
+
// categories: string[];
|
|
171
|
+
// how: string;
|
|
172
|
+
// }
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 解析顺序(每一步都是确定性的):
|
|
176
|
+
* 1. 大小写边界变换后精确命中
|
|
177
|
+
* 2. 去下划线后命中(吸收 crop_16_9 / grid_3x3 / star_purple500 这类不规则数字断词)
|
|
178
|
+
* 3. 上游改名别名表
|
|
179
|
+
*/
|
|
180
|
+
function resolveName(pascal, idx) {
|
|
181
|
+
const direct = caseTransform(pascal);
|
|
182
|
+
const exact = idx.exact.get(direct);
|
|
183
|
+
if (exact) return { material: direct, categories: exact, how: 'exact' };
|
|
184
|
+
|
|
185
|
+
const squashed = idx.squashed.get(direct.replace(/_/g, ''));
|
|
186
|
+
if (squashed && squashed.length > 0) {
|
|
187
|
+
// squashed 的值是「索引中的规范名」列表;上游仅有 add_chart/addchart 一组重名,
|
|
188
|
+
// 取字典序首项保证确定性。
|
|
189
|
+
const canonical = [...squashed].sort()[0];
|
|
190
|
+
const cats = idx.exact.get(canonical) ?? [];
|
|
191
|
+
return { material: canonical, categories: cats, how: 'underscore-insensitive' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const alias = ALIASES[direct];
|
|
195
|
+
if (alias) {
|
|
196
|
+
const cats = idx.exact.get(alias);
|
|
197
|
+
if (cats) return { material: alias, categories: cats, how: `alias (upstream renamed ${direct})` };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { material: null, categories: [], how: 'unresolved' };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** `MoreScreen` → `MorePage`,与 lottie-conversion-rules.md 的页面命名建议保持一致。 */
|
|
204
|
+
function toHarmonyPage(screen) {
|
|
205
|
+
const base = screen.replace(/(Screen|Activity|Fragment|Tab|Dialog)$/, '');
|
|
206
|
+
return base ? `${base}Page` : screen;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function walk(dir, ext, out = []) {
|
|
210
|
+
let entries;
|
|
211
|
+
try {
|
|
212
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
213
|
+
} catch {
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
for (const e of entries) {
|
|
217
|
+
if (e.isDirectory()) {
|
|
218
|
+
if (!SKIP_DIRS.has(e.name)) walk(path.join(dir, e.name), ext, out);
|
|
219
|
+
} else if (e.name.endsWith(ext)) {
|
|
220
|
+
out.push(path.join(dir, e.name));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─────────────────────────────────────────────
|
|
227
|
+
// 1. 从 dex 提取清单
|
|
228
|
+
// ─────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
function scanDex(dexDir) {
|
|
231
|
+
const found = new Set();
|
|
232
|
+
let dexFiles;
|
|
233
|
+
try {
|
|
234
|
+
dexFiles = fs.readdirSync(dexDir).filter((f) => /^classes\d*\.dex$/.test(f));
|
|
235
|
+
} catch {
|
|
236
|
+
fail(`--dex 目录不可读: ${dexDir}`);
|
|
237
|
+
}
|
|
238
|
+
if (dexFiles.length === 0) return found;
|
|
239
|
+
|
|
240
|
+
for (const f of dexFiles) {
|
|
241
|
+
// latin1 保证字节与字符一一对应;DEX 字符串池中这些名字是纯 ASCII。
|
|
242
|
+
const text = fs.readFileSync(path.join(dexDir, f)).toString('latin1');
|
|
243
|
+
for (const m of text.matchAll(DEX_ICON_RE)) found.add(m[0]);
|
|
244
|
+
}
|
|
245
|
+
return found;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─────────────────────────────────────────────
|
|
249
|
+
// 2. 从源码提取归属
|
|
250
|
+
// ─────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
// interface Attribution {
|
|
253
|
+
// screens: Set<string>;
|
|
254
|
+
// files: Set<string>;
|
|
255
|
+
// }
|
|
256
|
+
|
|
257
|
+
function scanSource(srcDir) {
|
|
258
|
+
const byIcon = new Map();
|
|
259
|
+
for (const file of walk(srcDir, '.kt')) {
|
|
260
|
+
let text;
|
|
261
|
+
try {
|
|
262
|
+
text = fs.readFileSync(file, 'utf8');
|
|
263
|
+
} catch {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!text.includes('Icons.')) continue;
|
|
267
|
+
|
|
268
|
+
const fileBase = path.basename(file, '.kt');
|
|
269
|
+
for (const m of text.matchAll(SRC_ICON_RE)) {
|
|
270
|
+
const auto = Boolean(m[1]);
|
|
271
|
+
const style = m[2] === 'Default' ? 'Filled' : m[2]; // Default 是 Filled 的别名
|
|
272
|
+
const key = `${auto ? 'AutoMirrored.' : ''}${style}.${m[3]}`;
|
|
273
|
+
|
|
274
|
+
// 归属证据优先级:最近的上文 `fun <Name>(` > 文件名
|
|
275
|
+
const before = text.slice(0, m.index ?? 0);
|
|
276
|
+
const funMatches = [...before.matchAll(/\bfun\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g)];
|
|
277
|
+
const enclosing = funMatches.length > 0 ? funMatches[funMatches.length - 1][1] : null;
|
|
278
|
+
const screen =
|
|
279
|
+
enclosing && /(Screen|Page|Tab|Dialog)$/.test(enclosing) ? enclosing : fileBase;
|
|
280
|
+
|
|
281
|
+
let a = byIcon.get(key);
|
|
282
|
+
if (!a) {
|
|
283
|
+
a = { screens: new Set(), files: new Set() };
|
|
284
|
+
byIcon.set(key, a);
|
|
285
|
+
}
|
|
286
|
+
a.screens.add(screen);
|
|
287
|
+
a.files.add(path.relative(srcDir, file).split(path.sep).join('/'));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return byIcon;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─────────────────────────────────────────────
|
|
294
|
+
// 3. 解析到上游图形
|
|
295
|
+
// ─────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
/** name → [category],排除 `symbols`(Material Symbols 平行副本,目录布局不同)。 */
|
|
298
|
+
function buildIndex(raw) {
|
|
299
|
+
const exact = new Map();
|
|
300
|
+
for (const key of Object.keys(JSON.parse(raw))) {
|
|
301
|
+
const sep = key.indexOf('::');
|
|
302
|
+
if (sep < 0) continue;
|
|
303
|
+
const cat = key.slice(0, sep);
|
|
304
|
+
const name = key.slice(sep + 2);
|
|
305
|
+
if (cat === 'symbols') continue;
|
|
306
|
+
const list = exact.get(name);
|
|
307
|
+
if (list) list.push(cat);
|
|
308
|
+
else exact.set(name, [cat]);
|
|
309
|
+
}
|
|
310
|
+
for (const list of exact.values()) list.sort();
|
|
311
|
+
|
|
312
|
+
const squashed = new Map();
|
|
313
|
+
for (const name of exact.keys()) {
|
|
314
|
+
const k = name.replace(/_/g, '');
|
|
315
|
+
const list = squashed.get(k);
|
|
316
|
+
if (list) list.push(name);
|
|
317
|
+
else squashed.set(k, [name]);
|
|
318
|
+
}
|
|
319
|
+
return { exact, squashed };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseRef(compose) {
|
|
323
|
+
const parts = compose.split('.');
|
|
324
|
+
const autoMirrored = parts[0] === 'AutoMirrored';
|
|
325
|
+
const rest = autoMirrored ? parts.slice(1) : parts;
|
|
326
|
+
return { style: rest[0], pascal: rest[1], autoMirrored };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─────────────────────────────────────────────
|
|
330
|
+
// 主流程
|
|
331
|
+
// ─────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
function parseArgs(argv) {
|
|
334
|
+
const args = {};
|
|
335
|
+
for (let i = 0; i < argv.length; i++) {
|
|
336
|
+
const a = argv[i];
|
|
337
|
+
if (!a.startsWith('--')) fail(`无法识别的参数: ${a}`);
|
|
338
|
+
const key = a.slice(2);
|
|
339
|
+
const next = argv[i + 1];
|
|
340
|
+
if (next && !next.startsWith('--')) {
|
|
341
|
+
args[key] = next;
|
|
342
|
+
i++;
|
|
343
|
+
} else {
|
|
344
|
+
args[key] = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return args;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function loadArtwork(
|
|
351
|
+
args,
|
|
352
|
+
ref,
|
|
353
|
+
) {
|
|
354
|
+
const snapshot = typeof args.snapshot === 'string' ? args.snapshot : null;
|
|
355
|
+
|
|
356
|
+
if (snapshot) {
|
|
357
|
+
const indexPath = path.join(snapshot, 'update', 'current_versions.json');
|
|
358
|
+
if (!fs.existsSync(indexPath)) fail(`--snapshot 下缺少 update/current_versions.json: ${indexPath}`);
|
|
359
|
+
return {
|
|
360
|
+
index: fs.readFileSync(indexPath, 'utf8'),
|
|
361
|
+
label: `vendored snapshot: ${snapshot}`,
|
|
362
|
+
get: async (upstream) => fs.readFileSync(path.join(snapshot, upstream), 'utf8'),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!args.fetch) {
|
|
367
|
+
fail('必须提供 --fetch(常规方式,从 pinned commit 拉取,需要访问 raw.githubusercontent.com)'
|
|
368
|
+
+ ' 或 --snapshot <dir>(仅用于无外网的机器)');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const base = `${RAW_BASE}${ref}/`;
|
|
372
|
+
const res = await fetch(`${base}update/current_versions.json`);
|
|
373
|
+
if (!res.ok) fail(`拉取上游索引失败: HTTP ${res.status}`);
|
|
374
|
+
return {
|
|
375
|
+
index: await res.text(),
|
|
376
|
+
label: `upstream google/material-design-icons@${ref}`,
|
|
377
|
+
get: async (upstream) => {
|
|
378
|
+
const r = await fetch(base + upstream);
|
|
379
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
380
|
+
return r.text();
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function main() {
|
|
386
|
+
const args = parseArgs(process.argv.slice(2));
|
|
387
|
+
const quiet = Boolean(args.quiet);
|
|
388
|
+
const dryRun = Boolean(args['dry-run']);
|
|
389
|
+
|
|
390
|
+
const dexDir = typeof args.dex === 'string' ? args.dex : null;
|
|
391
|
+
const outDir = typeof args.out === 'string' ? args.out : null;
|
|
392
|
+
const androidSrc = typeof args['android-src'] === 'string' ? args['android-src'] : null;
|
|
393
|
+
const ref = typeof args.ref === 'string' ? args.ref : DEFAULT_REF;
|
|
394
|
+
const attributedOnly = Boolean(args['attributed-only']);
|
|
395
|
+
|
|
396
|
+
if (!dexDir) fail('缺少 --dex <decoded apk dir>');
|
|
397
|
+
if (!outDir && !dryRun) fail('缺少 --out <base/media dir>(或使用 --dry-run)');
|
|
398
|
+
if (!fs.existsSync(dexDir)) fail(`--dex 路径不存在: ${dexDir}`);
|
|
399
|
+
if (androidSrc && !fs.existsSync(androidSrc)) fail(`--android-src 路径不存在: ${androidSrc}`);
|
|
400
|
+
|
|
401
|
+
// 1. 清单
|
|
402
|
+
const names = scanDex(dexDir);
|
|
403
|
+
if (names.size === 0) {
|
|
404
|
+
if (!quiet) console.log('[code_vector_icons] 未在 classes*.dex 中检出代码内定义图标引用。');
|
|
405
|
+
if (typeof args.json === 'string') {
|
|
406
|
+
const empty = {
|
|
407
|
+
ref,
|
|
408
|
+
artworkSource: 'n/a',
|
|
409
|
+
dexDir,
|
|
410
|
+
androidSrc,
|
|
411
|
+
outDir: outDir ?? '',
|
|
412
|
+
libraryLikelyUnshrunk: false,
|
|
413
|
+
attributedOnly,
|
|
414
|
+
counts: {
|
|
415
|
+
referencesFound: 0, resolved: 0, unresolved: 0,
|
|
416
|
+
mediaFilesWritten: 0, autoMirrored: 0,
|
|
417
|
+
screenAttributed: 0, screenUnknown: 0,
|
|
418
|
+
},
|
|
419
|
+
icons: [],
|
|
420
|
+
};
|
|
421
|
+
fs.writeFileSync(args.json, `${JSON.stringify(empty, null, 2)}\n`, 'utf8');
|
|
422
|
+
}
|
|
423
|
+
process.exit(0);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 2. 归属
|
|
427
|
+
const attribution = androidSrc ? scanSource(androidSrc) : new Map();
|
|
428
|
+
|
|
429
|
+
// 3. 图形来源
|
|
430
|
+
const artwork = await loadArtwork(args, ref);
|
|
431
|
+
const index = buildIndex(artwork.index);
|
|
432
|
+
|
|
433
|
+
const allNames = [...names].sort();
|
|
434
|
+
const unknownCount = allNames.filter((n) => !attribution.has(n)).length;
|
|
435
|
+
const libraryLikelyUnshrunk =
|
|
436
|
+
allNames.length >= UNSHRUNK_MIN_REFS && unknownCount / allNames.length >= UNSHRUNK_UNKNOWN_RATIO;
|
|
437
|
+
|
|
438
|
+
// 未裁剪判定必须在抓取循环之前生效,而不是循环之后才提示。
|
|
439
|
+
// 循环会为清单里每个名字发一次网络请求并落一个 svg:在未裁剪的 debug dex 上
|
|
440
|
+
// 这是数千次请求、数十分钟,且产物是整库图标而非应用实际用到的图标。等抓完
|
|
441
|
+
// 再打印「建议加 --attributed-only」,代价已经付掉了,且 base/media 已被污染,
|
|
442
|
+
// 必须人工清理后重跑。所以这里直接拒绝,把取舍交回调用方显式表达。
|
|
443
|
+
if (libraryLikelyUnshrunk && !attributedOnly) {
|
|
444
|
+
fail(
|
|
445
|
+
`dex 清单 ${allNames.length} 个图标中有 ${unknownCount} 个在源码中找不到引用` +
|
|
446
|
+
`(≥${Math.round(UNSHRUNK_UNKNOWN_RATIO * 100)}%),图标库很可能未被 R8 裁剪,整库被打进了 APK。\n` +
|
|
447
|
+
' 此时 dex 清单不等于「应用实际使用的图标」,照单抓取会产出数千个无用图形并污染 base/media。\n' +
|
|
448
|
+
' 请二选一:\n' +
|
|
449
|
+
' 1) 加 --attributed-only,只为源码真正引用到的图标产出图形(推荐);\n' +
|
|
450
|
+
' 2) 换用开启了 minify 的 release 包解码目录作为 --dex。',
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const icons = [];
|
|
455
|
+
for (const compose of allNames) {
|
|
456
|
+
const { style, pascal, autoMirrored } = parseRef(compose);
|
|
457
|
+
const res = resolveName(pascal, index);
|
|
458
|
+
const cats = res.categories;
|
|
459
|
+
const attr = attribution.get(compose);
|
|
460
|
+
const screens = attr ? [...attr.screens].sort() : [];
|
|
461
|
+
|
|
462
|
+
const icon = {
|
|
463
|
+
compose, style, pascal,
|
|
464
|
+
autoMirrored,
|
|
465
|
+
material: res.material ?? caseTransform(pascal),
|
|
466
|
+
category: cats.length > 0 ? cats[0] : null,
|
|
467
|
+
otherCategories: cats.slice(1),
|
|
468
|
+
upstream: null,
|
|
469
|
+
media: null,
|
|
470
|
+
screens: screens.length > 0 ? screens : ['Unknown'],
|
|
471
|
+
suggestedPage: screens.length === 1 ? toHarmonyPage(screens[0]) : null,
|
|
472
|
+
sourceRefs: attr ? [...attr.files].sort() : [],
|
|
473
|
+
resolved: false,
|
|
474
|
+
reason: '',
|
|
475
|
+
notes: [],
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
if (autoMirrored) {
|
|
479
|
+
icon.notes.push('AutoMirrored: mirror horizontally under right-to-left locales');
|
|
480
|
+
}
|
|
481
|
+
if (!attr) {
|
|
482
|
+
icon.notes.push('dex-only; no source reference found (likely used by a library)');
|
|
483
|
+
}
|
|
484
|
+
if (res.how !== 'exact' && res.material) {
|
|
485
|
+
icon.notes.push(`name resolved by ${res.how}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!res.material) {
|
|
489
|
+
icon.reason = 'not found in Material Icons index';
|
|
490
|
+
} else if (attributedOnly && !attr) {
|
|
491
|
+
// 图标库未裁剪时,dex 清单包含整个库;只为源码真正引用的图标产出图形,
|
|
492
|
+
// 其余仍然留行,但标记为不产出。
|
|
493
|
+
icon.reason = 'packaged by an unshrunk icon library; not referenced by application code';
|
|
494
|
+
} else {
|
|
495
|
+
if (cats.length > 1) {
|
|
496
|
+
icon.notes.push(`ambiguous category, alternatives: ${cats.slice(1).join(', ')}`);
|
|
497
|
+
}
|
|
498
|
+
icon.upstream = `src/${cats[0]}/${res.material}/${STYLE[style].dir}/24px.svg`;
|
|
499
|
+
icon.media = `ic_${res.material}_${STYLE[style].suffix}.svg`;
|
|
500
|
+
}
|
|
501
|
+
icons.push(icon);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 4. 取图形并落盘(同一 media 文件只取一次)
|
|
505
|
+
const wanted = new Map();
|
|
506
|
+
for (const i of icons) if (i.media && i.upstream) wanted.set(i.media, i.upstream);
|
|
507
|
+
|
|
508
|
+
if (outDir && !dryRun) fs.mkdirSync(outDir, { recursive: true });
|
|
509
|
+
|
|
510
|
+
const written = new Map();
|
|
511
|
+
for (const [media, upstream] of wanted) {
|
|
512
|
+
let body;
|
|
513
|
+
try {
|
|
514
|
+
body = await artwork.get(upstream);
|
|
515
|
+
} catch (e) {
|
|
516
|
+
for (const i of icons) {
|
|
517
|
+
if (i.media === media) i.reason = `artwork unavailable: ${(e).message}`;
|
|
518
|
+
}
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (!body.trimStart().startsWith('<svg') || !body.includes('viewBox="0 0 24 24"')) {
|
|
522
|
+
for (const i of icons) if (i.media === media) i.reason = 'upstream file is not a 24x24 svg';
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const cleaned = `${body.replace(GUARD_PATH_RE, '').trim()}\n`;
|
|
526
|
+
|
|
527
|
+
if (outDir && !dryRun) {
|
|
528
|
+
const target = path.join(outDir, media);
|
|
529
|
+
// 不覆盖 res/ 转换产出的同名资源:改名并在 notes 记录(见 rules §7)
|
|
530
|
+
if (fs.existsSync(target)) {
|
|
531
|
+
const renamed = media.replace(/\.svg$/, '_material.svg');
|
|
532
|
+
for (const i of icons) {
|
|
533
|
+
if (i.media === media) {
|
|
534
|
+
i.media = renamed;
|
|
535
|
+
i.notes.push(`renamed to avoid collision with a res/-derived media file (${media})`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
fs.writeFileSync(path.join(outDir, renamed), cleaned, 'utf8');
|
|
539
|
+
written.set(renamed, { bytes: cleaned.length });
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
fs.writeFileSync(target, cleaned, 'utf8');
|
|
543
|
+
}
|
|
544
|
+
written.set(media, { bytes: cleaned.length });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
for (const i of icons) {
|
|
548
|
+
if (i.media && written.has(i.media)) {
|
|
549
|
+
i.resolved = true;
|
|
550
|
+
i.bytes = written.get(i.media).bytes;
|
|
551
|
+
} else if (!i.reason) {
|
|
552
|
+
i.reason = 'artwork not retrieved';
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const unresolved = icons.filter((i) => !i.resolved);
|
|
557
|
+
const report = {
|
|
558
|
+
ref,
|
|
559
|
+
artworkSource: artwork.label,
|
|
560
|
+
dexDir,
|
|
561
|
+
androidSrc,
|
|
562
|
+
outDir: outDir ?? '',
|
|
563
|
+
libraryLikelyUnshrunk,
|
|
564
|
+
attributedOnly,
|
|
565
|
+
counts: {
|
|
566
|
+
referencesFound: icons.length,
|
|
567
|
+
resolved: icons.length - unresolved.length,
|
|
568
|
+
unresolved: unresolved.length,
|
|
569
|
+
mediaFilesWritten: written.size,
|
|
570
|
+
autoMirrored: icons.filter((i) => i.autoMirrored).length,
|
|
571
|
+
screenAttributed: icons.filter((i) => i.screens[0] !== 'Unknown').length,
|
|
572
|
+
screenUnknown: icons.filter((i) => i.screens[0] === 'Unknown').length,
|
|
573
|
+
},
|
|
574
|
+
icons,
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
if (typeof args.json === 'string') {
|
|
578
|
+
fs.writeFileSync(args.json, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (!quiet) {
|
|
582
|
+
const c = report.counts;
|
|
583
|
+
console.log(`[code_vector_icons] artwork: ${artwork.label}`);
|
|
584
|
+
console.log(` references found (dex) : ${c.referencesFound}`);
|
|
585
|
+
console.log(` resolved : ${c.resolved}`);
|
|
586
|
+
console.log(` unresolved : ${c.unresolved}`);
|
|
587
|
+
console.log(` media files written : ${c.mediaFilesWritten}${dryRun ? ' (dry-run)' : ''}`);
|
|
588
|
+
console.log(` auto-mirrored : ${c.autoMirrored}`);
|
|
589
|
+
console.log(` screen attributed : ${c.screenAttributed} (unknown: ${c.screenUnknown})`);
|
|
590
|
+
if (libraryLikelyUnshrunk) {
|
|
591
|
+
// 走到这里说明已显式加了 --attributed-only(否则上面的闸门已退出)。
|
|
592
|
+
console.log(
|
|
593
|
+
' NOTE: 图标库未被 R8 裁剪,dex 清单包含整库图标;已按 --attributed-only\n' +
|
|
594
|
+
' 只为源码引用到的图标产出图形,其余留行标记为 unmappable。',
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
for (const i of unresolved) {
|
|
598
|
+
console.log(` UNRESOLVED ${i.compose} (${i.material}) ${i.reason}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// --attributed-only 主动跳过的不算失败:它是有意的取舍,且仍然留了记录行。
|
|
603
|
+
const hardUnresolved = unresolved.filter((i) => !i.reason.startsWith('packaged by an unshrunk'));
|
|
604
|
+
process.exit(hardUnresolved.length > 0 ? 1 : 0);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
main().catch((e) => fail(String(e && (e).stack ? (e).stack : e)));
|