开源鸿蒙平台 CMP 三方库「compose-rules」适配全流程
本文记录
compose-rules接入 OpenHarmony 的完整过程,覆盖生态位盘点、构建期工具的适配路线选择、鸿蒙工具链换线、Kotlin 2.2 API 迁移、上游 190 项测试交叉验证、JVM 双引擎验收工程、ArkUI 报告页面、HAP 构建和真机验收。
compose-rules是 Twitter 开源的 Jetpack Compose 静态检查规则库:17 条规则同时以 ktlint 规则集与 detekt 规则集两种形态发布,规则跑在构建期的 JVM 上,用kotlin-compiler-embeddable解析 Compose 源码、报告违规。本次适配与社区运行时库路线(qrose / KStore / kstatemachine 的"加 ohos target 补 actual")有本质区别:这个库没有任何一行代码会出现在设备上——ktlint 与 detekt 在任何平台上都是构建期 JVM 程序,鸿蒙工程照旧在 JVM 上跑它们。因此适配走的是 kotlinx-benchmark 先例确立的构建期工具路线:不给模块加ohosArm64/ohosX64,而是把规则引擎依赖的kotlin-compiler-embeddable整体换到鸿蒙社区 Kotlin 制品线2.2.21-1.0.0,使 lint 引擎与 OpenHarmony 工程使用的 Kotlin 语言版本对齐;17 条规则逻辑零改动,上游 190 项测试全绿;再用一个 JVM 验收工程证明双引擎在鸿蒙线上真实咬合,一个 DevEco HAP 作为报告可视化载体完成真机验收。
项目地址: AtomGit/oh-tpc/compose-rules.git
上游仓库: twitter/compose-rules
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-rules 为 Compose 团队提供"写法规范"这一件事的完整答案:@Composable 命名、modifier 参数约定、状态上提、remember 纪律、不稳定集合、CompositionLocal 命名与 allowlist、Preview 可见性……17 条规则把最常见的 Compose 反模式在 code review 之前拦下来。规则实现层(rules/common)是纯 Kotlin PSI 遍历,通过两个适配层接进 ktlint(TwitterKtlintRule)和 detekt(TwitterDetektRule),一条规则逻辑服务两个引擎。KMP/CMP 项目把这些检查用在共享 UI 层上,OpenHarmony 的 CMP 工程同样需要——但上游构建(Kotlin 1.8.0 / Gradle 7.4.2)的解析器比鸿蒙工具链落后整整四代,鸿蒙 CMP 工程用 HarmonyOS Kotlin 2.2.21 编译,源码里会出现 2.x 语法,1.8 的 lint 引擎与 2.2 的工程代码之间没有语言版本对齐的保证。
如果只把规则文档翻译一遍贴到鸿蒙社区,文档可以看,却无法证明规则引擎在鸿蒙工具链线上完成编译、解析和报告。适配需要同时验证规则库本体、引擎运行时兼容、验收工程、报告管线、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 工具链不一致 | 上游 Kotlin 1.8.0 / Gradle 7.4.2,鸿蒙社区线 Kotlin 2.2.21-1.0.0 只发布在 eazytec Maven;KGP 2.2 还要求 Gradle 7.6.3+,wrapper 必须升。 |
| API 断层 | Kotlin 2.2 移除了 KtStubElementTypes.FILE、把 KtPsiFactory(element) 构造器和 addToStdlib.cast 标记为废弃/不安全,上游开着 -Werror,三处源码必须迁移。 |
| 插件断层 | Gradle 8.14 下 shadow 7.1.2 与 spotless 6.13.0 使用已移除的 Gradle API,必须随 wrapper 一起升。 |
| JVM 字节码校验 | KGP 2.2 强校验 Java/Kotlin 编译目标一致性,JDK 21 下 compileJava(21) 对 compileKotlin(11) 直接失败。 |
| 验收形态特殊 | lint 库没有可跑在设备上的运行时,不能照抄"so + NAPI + HAP"套路,验收载体要重新定义。 |
| 交付链路复杂 | 报告管线、rawfile 资源、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:规则库整体换线编译、三处 API 迁移和双验收载体(JVM 扫描 + HAP 报告页)。规则语义完全由上游原版实现负责,ArkTS 只负责读取报告和展示结果。
1.2 库提供的能力
Demo 提供一次真实的静态扫描会话,对应适配的全部入口:
- 双引擎扫描(ktlint 面):
TwitterComposeRuleSetProvider注册全部 17 条规则,KtLint.lint逐文件解析样本并收集LintError(行、列、规则 ID、消息); - 双引擎扫描(detekt 面):17 个
Compose*Check(Config.empty)实例经detekt-test的lint(code)逐规则扫描同一份样本; - 门禁:每个引擎 ≥10 条违规且 ≥6 条不同规则,否则任务退出码 1——规则引擎哪天静默失效,验收立刻变红;
- 报告管线:扫描结果落
compose_rules_report.json,经packageLintReport任务自动拷入 DevEco 工程 rawfile,HAP 报告页展示的是真机可见的真实数据; - 规则目录:17 条规则按 State / Composables / Modifiers 三组在 HAP 内静态呈现,含一句话说明与真实规则 ID。
响应报告结构:
| 字段 | 作用 |
|---|---|
toolchain | 本次扫描用的 Kotlin / Gradle / JDK / 规则版本(来自真实构建环境,非静态文字)。 |
ktlint.violations[] / detekt.violations[] | 逐条违规:文件名、行、列、规则 ID、消息。 |
summary | 两引擎违规总数。 |
规则侧完整能力(命名、modifier 约定、状态纪律、不稳定集合、allowlist 配置)全部在上游 rules/common 里原样保留,JVM 侧保留 190 项上游测试做交叉验证,证明换线后的规则逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 六个模块全部原样编译,不提取、不重写;仅 3 个文件 5 处调用点做 Kotlin 2.2 API 等价迁移,17 条规则逻辑零改动。 |
| 平台目标 | 纯 JVM 构建,不加 ohos target(构建期工具,kotlinx-benchmark plugin 面先例);example/lint-demo 承担验收,JVM 即目标环境。 |
| 构建隔离 | 仓库按已审核标准重排(eazytec 双仓置顶);Sonatype/vanniktech 发布链整体剥离;上游首页存 README.upstream.md。 |
| 管线稳定 | 报告走 JSON 文件 + rawfile 资源,不传递对象引用;扫描门禁保证"没咬到违规就红"。 |
| 依赖纪律 | ktlint 0.48.2 / detekt 1.22.0 与上游完全一致(换线不换版),只把 org.jetbrains.kotlin 全线换到鸿蒙制品线,上游测试零改动全绿。 |
| UI 完整 | 单页三 Tab 覆盖全部验收面:规则目录、扫描报告(工具链/计数/逐条违规)、适配说明。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是规则引擎的运行宿主。
compose-rules是构建期工具,HAP 报告页是验收载体——把构建期扫描结果画出来,证明这条管线在鸿蒙工程语境里闭环。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认构建期工具路线、确定交付物
第 2 阶段:工具链换线 ── 仓库重排(eazytec 置前)、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1 + JDK 21、发布链剥离
第 3 阶段:API 迁移 ── KtPsiFactory.contextual、KtFileElementType.INSTANCE、cast → as,三文件五调用点
第 4 阶段:验收工程 ── lint-demo 双引擎 runner、违规样本、扫描门禁、JSON 报告
第 5 阶段:ArkUI 宿主 ── 三 Tab 页面、rawfile 报告读取、规则目录、适配说明
第 6 阶段:示例与验证 ── 190 项上游测试、验收门禁、HAP、签名、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
core-common/ 引擎无关规则核心(PSI visitor、Emitter)(进构建)
core-ktlint/ core-detekt/ ktlint / detekt 引擎适配层(进构建)
rules/common/ 17 条规则实现(进构建,除 1 行外零改动)
rules/ktlint/ rules/detekt/ 规则集提供者 + shadow uber jar(进构建)
example/lint-demo/ JVM 验收工程(样本 + 双引擎 runner + 报告管线)
example/harmonyApp/ DevEco Stage、纯 ArkTS 三 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
docs/ .github/ spotless/ mkdocs.yml 上游文档站与 CI,留盘不动
构建拓扑对齐 cmp_qrose 已审核模式:settings.gradle 的 rootProject.name = "ohos_compose-rules",include 六个规则模块与 :example:lint-demo;上游的 gradle-enterprise 插件块、vanniktech 发布 classpath、Sonatype POM 键全部剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose / mordant / kotlinx-benchmark 同线) |
| JDK | Temurin 21(org.gradle.java.home 写进 gradle.properties) |
| Gradle | 8.14.1(wrapper 从 7.4.2 升级,KGP 2.2 要求 7.6.3+) |
| 引擎依赖 | ktlint 0.48.2 / detekt 1.22.0(与上游一致,换线不换版) |
| 插件 | spotless 6.13.0 → 6.25.0、shadow 7.1.2 → 8.1.1(Gradle 8.14 兼容) |
| 字节码 | Kotlin jvmTarget 11 + JavaCompile options.release = 11(与上游产物一致) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| Bundle | org.terminator.ohos.composerules |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 模块 | 源码 | 依赖形态 | 处置 |
|---|---|---|---|
core-common | 引擎无关核心 | kotlin-compiler-embeddable(api) | 整体换线编译 |
core-ktlint / core-detekt | 引擎适配层 | ktlint-core / detekt-core(JVM-only) | 整体编译 + 1~2 处 API 迁移 |
rules/common | 17 条规则 | 仅依赖 core-common | 整体编译 + 1 行迁移 |
rules/ktlint / rules/detekt | 提供者 + uber jar | shadow 插件 | 整体编译,插件升版 |
关键判断在依赖形态:全仓 JVM-only 依赖,没有一处 expect/actual,没有一处平台源码集——这正是"构建期工具不加 ohos target"的技术依据(详见决策 1)。
1.4 页面边界
标题区 COMPOSERULES · OPENHARMONY / 副标题(Compose 静态检查规则库 · 17 条规则)
Tab 切换 规则 / 报告 / 适配
规则 Tab State(4)/ Composables(9)/ Modifiers(4) 三组 17 条,每条规则名 + 说明 + 规则 ID
报告 Tab 工具链四行(Kotlin/Gradle/JDK/规则版本)
两引擎违规计数(filesScanned / 违规列表长度)
逐条违规(文件:行 + 规则 ID + 消息,可滚动)
适配 Tab 构建期工具定位、换线方式、验收路径、仓库与社区链接
页面启动时在 aboutToAppear 里同步读 rawfile 报告并填充 @State,占位数据带黄色横幅提示。
第 2 阶段:工具链换线
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上鸿蒙 Kotlin 线的制品是否齐全。用 POM 逐个验证:
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.21-1.0.0 → HTTP 200
org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21-1.0.0 → eazytec 独有(中央仓库无此版本)
三个关键结论:其一,鸿蒙线 Kotlin 全家桶(gradle-plugin / stdlib / compiler-embeddable)带 -1.0.0 后缀只在 eazytec 发布,仓库顺序必须置前;其二,ktlint 0.48.2 与 detekt 1.22.0 是标准 JVM 制品,照常从 Maven Central 解析,与鸿蒙线无冲突;其三,kotlin-compiler-embeddable 2.2.21-1.0.0 就是规则引擎的解析器本体——版本目录里 kotlin = "2.2.21-1.0.0" 一处改动,构建插件与解析引擎同时换线。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
上游是 Kotlin 1.8.0——鸿蒙线没有 1.x 对应版本,必须跨代升级。选 2.2.21-1.0.0 而不是更新的 2.4.20-0.1.0-07,依据是"换线不换版、就低不就高"的已审核钉法:qrose、mordant、kotlinx-benchmark 三个仓库都用 2.2.21 线,本库规则源码是 1.8 时代代码,2.2.21 是兼容距离最小的鸿蒙线。Gradle 随之钉 8.14.1:KGP 2.2.21 要求 Gradle 7.6.3+,同时 2.2.21 线的全部先例都在 8.14.1(2.4.20 线才允许 Gradle 9.x),不越线。
settings.gradle 把 eazytec 仓库写在 pluginManagement 最前:
pluginManagement {
repositories {
// HarmonyOS Kotlin toolchain (eazytec community line) must come first.
maven { url 'https://maven.eazytec-cloud.com/nexus/repository/maven-public/' }
maven { url 'https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos' }
gradlePluginPortal()
mavenCentral()
}
}
rootProject.name = "ohos_compose-rules"
根 build.gradle 的 buildscript / allprojects 仓库同步置前。gradle.properties 钉 JDK 并改版本:
org.gradle.java.home=C:/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot
GROUP=com.twitter.compose.rules
VERSION_NAME=0.0.26-ohos.1
2.3 换线第一轮编译的连环雷
wrapper 升 8.14.1、Kotlin 升 2.2.21 后,第一轮编译暴露四类问题,全部有解:
| 问题 | 现象 | 处置 |
|---|---|---|
| JVM target 不一致 | compileJava (21) vs compileKotlin (11) 失败 | 根构建加 JavaCompile options.release = 11 |
| 移除的增量属性 | useClasspathSnapshot 每次构建警告 | 从 gradle.properties 删除 |
| 插件不兼容 | shadow 7.1.2 / spotless 6.13.0 用已移除 API | 升 8.1.1 / 6.25.0 |
| 源码 API 断层 | 三文件编译失败(-Werror) | 见第 3 阶段 |
2.4 发布链剥离
上游通过 vanniktech 插件发 Maven Central。fork 侧:根 build.gradle 删 classpath,六个模块各删一行 apply plugin: "com.vanniktech.maven.publish",gradle.properties 删 SONATYPE_HOST / RELEASE_SIGNING_ENABLED / 全部 POM_* 键。产物消费走本地构建或社区 Maven。
第 3 阶段:API 迁移(技术难点)
上游开 allWarningsAsErrors = true,Kotlin 2.2 里三处旧 API 从"能用"变成"警告即错误",全部做语义等价替换——这是整个 fork 仅有的源码改动:
3.1 ComposeViewModelInjection.kt:KtPsiFactory 构造器废弃
// before(Kotlin 1.8)
val factory = KtPsiFactory(parameterList)
// after(Kotlin 2.2:官方迁移目标,行为一致——用元素的 project/module 上下文建工厂)
val factory = KtPsiFactory.contextual(parameterList)
这行在 vm-injection 规则的自动修复路径上,ktlint 面 103 项测试里含自动修复场景的用例直接覆盖它。
3.2 TwitterKtlintRule.kt:FILE 常量消失 + cast 不安全
javap 检查 2.2.21 的 embeddable jar,KtStubElementTypes 里已无 FILE 字段,文件级元素类型移到单例 KtFileElementType.INSTANCE;addToStdlib.cast<T>() 被标记为错误级"unsafe cast"警告:
// before
KtStubElementTypes.FILE -> visitFile(psi.cast(), autoCorrect, emit.toEmitter())
// after
KtFileElementType.INSTANCE -> visitFile(psi as KtFile, autoCorrect, emit.toEmitter())
三处 psi.cast<...>() 逐一换成 as + @Suppress("UNCHECKED_CAST"),cast 目标类型与原来相同(KtFile / KtClass / KtFunction)。
3.3 TwitterDetektRule.kt:同款 cast 迁移
element.cast<KtFunction>() → element as KtFunction,一处。
五个调用点改完,190 项上游测试全绿——迁移等价性由测试直接证明(见第 6 阶段)。
第 4 阶段:验收工程(报告管线)
4.1 数据流
samples/Violations*.kt(故意违规的 Compose 样本,只解析不编译)
↓ TwitterComposeRuleSetProvider().getRuleProviders() / 17 个 Check(Config.empty)
LintDemoRunner.kt(ktlint 面 KtLint.lint + detekt 面 lint(code))
↓ 门禁:每引擎 ≥10 条违规且 ≥6 条不同规则,否则 exitProcess(1)
build/lint-report/compose_rules_report.json
↓ packageLintReport(Copy 任务,runLintDemo finalizedBy)
example/harmonyApp/entry/src/main/resources/rawfile/compose_rules_report.json
↓ DevEco 构建
HAP 报告页(ArkTS JSON.parse + @State 渲染)
4.2 样本设计
两份样本覆盖规则面的主干,全部从上游 docs/rules.md 的 Noncompliant 示例演化而来:
ViolationsProfileScreen.kt(状态类):小写 CompositionLocal(命名 + allowlist)、函数体里取 ViewModel(注入)、ViewModel 参数(转发)、mutableStateOf不套remember、小写 Composable、无 modifier 参数的 emitter;ViolationsSettingsScreen.kt(组件类):sealed interface + 尾随逗号的现代语法、@ComposableModifier 工厂函数、modifier 无默认值、根布局复用传入 modifier、多 emitter、emit 同时返回值、ArrayList/MutableState可变参数、List不稳定集合、小写 multipreview 注解、公开 preview。
样本只被解析不被编译,不需要任何 Compose 依赖;放进 samples/ 目录而非源码集,避免被模块编译。
4.3 门禁与对称性
val gatesPassed = byEngine.entries.all { (_, v) -> v.size >= 10 } &&
distinctRulesByEngine.entries.all { (_, rules) -> rules.size >= 6 }
if (!gatesPassed) exitProcess(1)
实测:两引擎各报 24 条、命中同一组 14 条规则。两个引擎共享同一套 rules/common 实现——计数对称是适配层的交叉验证:如果 ktlint 或 detekt 适配层被 2.2 迁移改坏,对称立刻破。未命中的 3 条规则(preview-public / parameter-order / vm-forwarding)是样本未构造出完整触发条件,它们在 190 项上游单测中全部有覆盖。
4.4 报告契约
{
"generatedAt": "…", "toolchain": { "kotlin": "2.2.21-1.0.0", "gradle": "8.14.1", "jdk": "21", "rulesVersion": "0.0.26-ohos.1" },
"ktlint": { "filesScanned": 2, "violations": [ { "file": "…", "line": 12, "col": 5, "ruleId": "twitter-compose:modifier-missing-check", "message": "…" } ] },
"detekt": { "filesScanned": 2, "violations": [ … ] },
"summary": { "ktlintViolations": 24, "detektViolations": 24 }
}
工具链四行来自构建环境注入的系统属性(gradle.version / rules.version / KotlinVersion.CURRENT),不是静态文字——报告页展示的就是本次真实构建的指纹。
第 5 阶段:ArkUI 宿主
5.1 读取 rawfile 报告
aboutToAppear(): void { this.loadReport(); }
private loadReport(): void {
try {
const context = this.getUIContext().getHostContext();
if (context === undefined || context === null) { this.reportError = '…'; return; }
const rm: resourceManager.ResourceManager = context.resourceManager;
const bytes: Uint8Array = rm.getRawFileContentSync(REPORT_FILE);
const text: string = util.TextDecoder.create('utf-8').decodeToString(bytes);
const parsed = JSON.parse(text) as LintReport;
/* 填充 @State:toolchainRows / ktlintViolations / detektViolations … */
} catch (e) {
const err = e as Error;
this.reportError = `读取或解析 ${REPORT_FILE} 失败:${err.message ?? '未知错误'}`;
}
}
全 interface 建 model(LintReport / ToolchainInfo / KtlintViolation…),无 any;ForEach 带keyGenerator;报告缺字段、rawfile 缺文件、JSON 非法三种情况都有兜底,不崩溃。
5.2 Kit 导入的正确答案(真机调试踩出来的)
resourceManager 的 Kit 导入在真机编译时报错两次:先写 @kit.AbilityKit 报 has no exported member 'resourceManager',改 @kit.ResourceManagementKit 又报 Cannot find module。最终用本机 SDK 的 d.ts 文件定案——resourceManager 由 @kit.LocalizationKit 导出:
// E:/DE/DevEco Studio/sdk/default/openharmony/ets/kits/@kit.LocalizationKit.d.ts
export { i18n, intl, resourceManager, sendableResourceManager }
结论:Kit 归属以本机 SDK 的 d.ts 为准,网上示例代码的导入路径不能直接抄。
5.3 页面交互
三个 Tab 底部切换。规则 Tab 三组 17 条白卡片滚动;报告 Tab 顶部四行工具链、中部两引擎计数、下部逐条违规列表;适配 Tab 说明构建期工具定位、换线方式与验收路径,底部仓库 / 社区链接。占位报告(未跑验收时)带黄色横幅提示先执行 runLintDemo。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── lint-demo/
│ ├── build.gradle runLintDemo(JavaExec)+ packageLintReport(Copy → rawfile)
│ ├── samples/Violations*.kt 违规样本(只解析不编译)
│ └── src/main/kotlin/demo/LintDemoRunner.kt 双引擎 runner + 门禁 + JSON 报告
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.composerules、versionName 0.0.26-ohos.1
└── entry/
├── src/main/ets/entryability/EntryAbility.ets
├── src/main/ets/pages/Index.ets 三 Tab 验收页面
├── src/main/resources/rawfile/compose_rules_report.json 真实扫描报告
└── src/main/module.json5 / resources(base/element、media、profile)
6.2 构建、签名和安装
.\gradlew.bat build # 六模块编译 + 190 项测试 + spotless
.\gradlew.bat :example:lint-demo:runLintDemo # 验收扫描,报告自动拷入 rawfile
# DevEco Studio 打开 example/harmonyApp(不要打开仓库根)
# File → Project Structure → Signing Configs 勾选自动签名
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc list targets -v
hdc -t <设备序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.composerules -m entry
四、完整代码对照
4.1 整体架构
twitter/compose-rules(上游 17 条规则,除 3 文件 5 调用点外原样编译)
│ rules/common:纯 Kotlin PSI 规则逻辑
│ core-ktlint / core-detekt:引擎适配层
Kotlin 2.2.21-1.0.0 制品线(eazytec)
│ kotlin-compiler-embeddable = 规则解析器本体
example/lint-demo
│ 双引擎 runner + 门禁 + JSON 报告
packageLintReport
│ 报告拷入 rawfile
example/harmonyApp
│ ArkTS JSON.parse + @State 渲染
▼
ArkUI 真机三 Tab 页面(规则目录 / 扫描报告 / 适配说明)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle | pluginManagement eazytec 置前、ohos_compose-rules、include lint-demo、gradle-enterprise 剥离 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 钉版、spotless 6.25.0 / shadow 8.1.1、发布插件删除 |
build.gradle(根) | 仓库置前、vanniktech 剥离、:example 免 spotless、options.release = 11 |
gradle.properties | JDK 21 钉版、VERSION_NAME=0.0.26-ohos.1、POM/Sonatype 键删除 |
rules/common/.../ComposeViewModelInjection.kt | KtPsiFactory.contextual 迁移(1 处) |
core-ktlint/.../TwitterKtlintRule.kt | KtFileElementType.INSTANCE + cast → as(4 处) |
core-detekt/.../TwitterDetektRule.kt | cast → as(1 处) |
example/lint-demo/build.gradle | runLintDemo + packageLintReport 报告管线 |
lint-demo/.../LintDemoRunner.kt | 双引擎扫描、门禁、JSON 报告 |
harmonyApp/.../pages/Index.ets | ArkUI 三 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(20 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| ktlint 面 | TwitterComposeRuleSetProvider().getRuleProviders() | 一次性取全部 17 条规则的 Provider |
| ktlint 面 | KtLint.lint(KtLint.ExperimentalParams(...)) | 0.48.2 的引擎入口(Params 是 0.49+ API,不能写错) |
| detekt 面 | Compose*Check(Config.empty) + lint(code) | 逐规则实例化扫描(每文件新实例,规则有状态) |
| 规则层 | visitComposable(function, autoCorrect, emitter) | 引擎无关的 Composable 访问钩子(上游原样) |
| 报告 | compose_rules_report.json | 两引擎统一契约,rawfile 交付 |
| ArkTS | getRawFileContentSync + TextDecoder.decodeToString | rawfile 读取与解码 |
| ArkTS | resourceManager(from @kit.LocalizationKit) | 资源管理入口(Kit 归属以本机 SDK 为准) |
4.4 验收 runner 与规则的边界
// ktlint 面
val ruleProviders = TwitterComposeRuleSetProvider().getRuleProviders()
KtLint.lint(
KtLint.ExperimentalParams(
fileName = sample.absolutePath,
text = sample.readText(),
ruleProviders = ruleProviders,
cb = { err, _ -> errors.add(err) },
),
)
// detekt 面(每文件新实例:detekt 规则有状态,不能跨文件复用)
for (check in newDetektChecks()) {
check.lint(code).forEach { finding -> results += Violation(…, finding.id, finding.message) }
}
两者之间只传文件文本与违规结果,不共享可变状态;LintError 与 Finding 各自映射进同一份 Violation model。
五、关键决策说明
决策 1:不加 ohosArm64/ohosX64,走构建期工具路线
加 ohos target 的前提是"这个库的代码会出现在设备上"。KStore 的 actual、qrose 的编码器满足前提;compose-rules 的全部依赖(kotlin-compiler-embeddable、ktlint-core、detekt-core)是 JVM-only,规则永远在构建期 JVM 里执行。给 JVM-only 模块强加 ohos target 编不出 klib,也没有运行场景。kotlinx-benchmark 的 plugin 模块已审核通过同款处置。适配的正确贡献是让规则理解鸿蒙工程的代码:解析器换到鸿蒙线 2.2.21,语言版本与 OpenHarmony 工程对齐。
决策 2:Kotlin 钉 2.2.21-1.0.0,Gradle 钉 8.14.1
上游 1.8.0 在鸿蒙线没有对应版本,必须跨代。2.2.21 线有三个已审核先例(qrose / mordant / kotlinx-benchmark),对 1.8 时代源码的兼容距离最小;2.4.20 线虽然更新,但先例全是 Kotlin 2.4 上游,且 Gradle 9.x 组合没有 2.2 代码的迁移先例。Gradle 8.14.1 是 2.2.21 线的标准搭配(该线不支持 Gradle 9)。插件(shadow / spotless)随 Gradle 升,依赖(ktlint / detekt)保持上游版本——“换线不换版”。
决策 3:三处源码迁移而不是关掉 -Werror
Kotlin 2.2 断层出现时,最省事的做法是把 allWarningsAsErrors 关掉——这会让 17 条规则与两个适配层暴露在真实的静默退化里。选择逐处迁移(5 个调用点、语义逐一等价),用 190 项测试收口:-Werror 保持开启,上游测试零改动全绿,迁移等价性才有证明。
决策 4:验收载体 = lint-demo(JVM)+ harmonyApp(HAP),不是 NAPI 桥
lint 库没有可桥接的运行时,照抄"so + NAPI + HAP"会制造一个假运行时。等价物是:lint-demo 用鸿蒙线解析器扫描鸿蒙风格的 Compose 代码(对应 qrose 的 nativeApp),harmonyApp 把扫描报告画到真机上(对应 harmonyApp 验收页)。报告页的数据源是构建期真实产物,不是摆设文字。
决策 5:报告走文件管线(rawfile),不走运行时桥
扫描发生在构建期,报告天然是静态产物。runLintDemo → compose_rules_report.json → packageLintReport → rawfile → HAP 的文件管线让"构建期扫描"与"真机展示"解耦:HAP 重新打包即可刷新报告,页面读取失败有完整兜底。这比在设备上跑一个假扫描诚实,也比手改 JSON 可复现。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 10(x64)、Temurin JDK 21、Gradle 8.14.1、DevEco Studio 5.1.0 Release(SDK default,compatibleSdkVersion 5.1.0(18))、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机。
6.2 JVM 交叉验证
.\gradlew.bat build
上游全量测试 190 项全部通过(测试文件零改动):
| 模块 | 用例数 | 钉住什么 |
|---|---|---|
rules/ktlint(18 个测试类) | 103 | 每条规则的触发 / 不触发 / 自动修复场景,经 ktlint 适配层 |
rules/detekt(18 个测试类) | 87 | 同一套规则逻辑经 detekt 适配层 |
这组测试同时证明两件事:其一,三处 API 迁移没有改变规则行为;其二,ktlint 0.48.2 与 detekt 1.22.0 的引擎在 kotlin-compiler-embeddable 2.2.21-1.0.0(鸿蒙线)上运行时二进制兼容——PSI shaded 包在 1.8 → 2.2 之间保持了兼容,依赖图被抬到 2.2.21 后引擎照常工作。build 还包含 spotless 格式检查与 shadow uber jar 组装,产物形态完好。
6.3 验收工程验证
:example:lint-demo:runLintDemo
[ktlint] 24 violations, 14 distinct rules
[detekt] 24 violations, 14 distinct rules
Acceptance gates passed: ktlint=24 detekt=24
Report written to …\example\lint-demo\build\lint-report\compose_rules_report.json
BUILD SUCCESSFUL
两引擎计数完全对称(共享 rules/common),门禁通过,报告经 packageLintReport 自动拷入 harmonyApp rawfile。
6.4 HAP 验证
Build ArkTS: finished(Kit 导入修正为 @kit.LocalizationKit 后)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
HAP 为纯 ArkTS 工程,无 native 三件套,体积远小于带 so 的 CMP demo;签名由 DevEco 自动生成,证书不进仓库。
6.5 功能用例
- 启动后底部三 Tab 可切换:规则 / 报告 / 适配;
- 规则 Tab 三组 17 条规则完整滚动,每条含说明与真实规则 ID;
- 报告 Tab 无黄色占位横幅——说明已加载真实报告(预置占位数据会显示横幅与提示);
- 工具链四行显示 Kotlin
2.2.21-1.0.0/ Gradle8.14.1/ JDK 21 / 规则版本0.0.26-ohos.1,与构建环境一致; - 两引擎计数各 24 条、14 条不同规则,逐条违规(文件:行 + 规则 ID + 消息)可滚动查看;
- 适配 Tab 展示构建期工具定位、换线方式与 AtomGit 仓库 / 社区链接。
6.6 验证结论
规则库本体、双引擎运行时、验收门禁、报告管线、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,报告页展示了真实的构建期扫描会话(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-rules.git
cd compose-rules
export JAVA_HOME="/path/to/jdk-21"
./gradlew build # 编译 + 190 项上游测试 + spotless
./gradlew :example:lint-demo:runLintDemo # 双引擎验收扫描 + 报告落 rawfile
# DevEco Studio 打开 example/harmonyApp,配置签名后:
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.composerules -m entry
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改文档不算适配,必须让规则库在鸿蒙工具链线上真实编译、测试、扫描——但也不能反向硬加 ohos target:JVM-only 依赖编不出 klib,构建期工具没有设备端场景;
settings.gradle里 eazytec 双仓必须排在pluginManagement最前,否则鸿蒙线 Kotlin 插件解析不到,配置期直接失败;- KGP 2.2 强校验 Java/Kotlin 编译目标一致性:JDK 21 下
compileJava默认 21,必须options.release = 11配平 Kotlin 的 jvmTarget 11,否则Inconsistent JVM Target Compatibility直接失败; - Kotlin 2.2 移除了
KtStubElementTypes.FILE(javap 确认),文件级元素类型用KtFileElementType.INSTANCE;addToStdlib.cast升级为错误级警告,换as+@Suppress("UNCHECKED_CAST")——上游-Werror让这些问题全部前置到编译期暴露; - Gradle 8.14 下 shadow 7.1.2 直接不可用,spotless 6.13.0 也有兼容问题;shadow → 8.1.1、spotless → 6.25.0 一次到位;
- Windows +
core.autocrlf=true检出的工作区是 CRLF,spotless 按上游 LF 规范校验会大面积红——跑一次spotlessApply归一化,对 git 提交内容无影响; - ktlint 0.48.2 的引擎入口是
KtLint.lint(KtLint.ExperimentalParams(...)),KtLint.Params是 0.49+ 的 API;引擎对象整体带 deprecation-for-removal 标记,runner 需要@file:Suppress("DEPRECATION")(0.49 换KtLintRuleEngine是上游新版本的活); - detekt 规则实例有状态,跨文件复用会串结果——每个文件重新实例化 17 个 Check;
resourceManager的 Kit 归属要以本机 SDK 为准:@kit.AbilityKit不导出它、@kit.ResourceManagementKit不存在,答案是@kit.LocalizationKit(导出{ i18n, intl, resourceManager, sendableResourceManager }),用 SDK 的 d.ts 文件定案,不抄网上示例;- 报告文件名两侧必须逐字符一致:runner 产出
compose-rules-report.json、HAP 读compose_rules_report.json,差一个连字符页面就永远显示占位数据——统一为下划线命名; kotlin.incremental.useClasspathSnapshot在 Kotlin 2.2 已移除,留在 gradle.properties 里每次构建都警告;- 签名材料由 DevEco 自动写入
build-profile.json5,仓库内signingConfigs保持空数组,*.hap/*.p12/*.cer进 gitignore,证书绝不进入 AtomGit。
8.2 已知问题
- ktlint 锁 0.48.2、detekt 锁 1.22.0(上游 0.0.26 的钉版;上游后续版本迁移 ktlint 0.49+
KtLintRuleEngine与 detekt 1.23 新 Provider API,fork 跟随上游节奏,不抢跑); - 规则是 PSI 语法级的(与上游一致),不做完整类型推断,不检查 ArkTS(.ets)代码——检查对象是鸿蒙 CMP 工程共享 UI 层的 Kotlin Compose 源码;
- 报告页数据在 HAP 打包时固化,重新扫描后需重新打包 HAP(构建期产物的天然属性);
- 发布链已剥离,产物消费走本地构建或社区 Maven,不发 Maven Central;
- HAP 报告页读取失败时显示错误卡片(设计如此),需要重跑
runLintDemo后重打包。
8.3 未来优化方向
- 把
com.twitter.compose.rules:*:0.0.26-ohos.1发布到社区 Maven,鸿蒙工程直接消费坐标; - 接 CI:AtomGit 流水线跑
build+runLintDemo,双门禁化(测试 + 扫描对称性); - 跟踪上游 0.0.27+ 的 ktlint/detekt 大版本迁移,复用本次的三类 API 迁移方法;
- 验收样本扩展到全部 17 条规则命中(补 preview-public / parameter-order / vm-forwarding 的触发构造);
- 评估把扫描报告接入 DevEco 工程的 ohpm/hvigor 生命周期,让鸿蒙工程一条命令出报告。
九、总结
9.1 核心难点回顾
上游 compose-rules 0.0.26(Kotlin 1.8 / Gradle 7.4.2)
→ 路线判定:构建期纯 JVM 工具,不加 ohos target(kotlinx-benchmark 先例)
→ 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21(eazytec 置前)
→ 发布链剥离 + 插件升版(shadow 8.1.1 / spotless 6.25.0)
→ 三文件五调用点 API 迁移(-Werror 全程保持)
→ 190 项上游测试全绿(换线不换版的直接证据)
→ lint-demo 双引擎验收(24 + 24 对称 + 门禁)
→ 报告文件管线(rawfile)→ ArkTS 三 Tab 页面
→ HAP 构建、签名、真机验收
9.2 封装层次
- 规则层:上游原版 17 条规则实现(
rules/common),3 文件 5 调用点 API 等价迁移; - 引擎层:上游原版 ktlint / detekt 适配层,跑在鸿蒙线
kotlin-compiler-embeddable 2.2.21-1.0.0上; - 构建层:eazytec 双仓置前、JDK 21 / Gradle 8.14.1 钉版、发布链剥离;
- 验收层:lint-demo 双引擎 runner、违规样本、扫描门禁、JSON 报告;
- 管线层:runLintDemo → compose_rules_report.json → rawfile;
- ArkTS 层:三 Tab 页面、rawfile 读取、JSON 解析、@State 渲染;
- DevEco 层:完成 HAP、签名、安装和真机运行。
9.3 三条经验
- 适配路线由"代码在哪执行"决定,不由"库叫什么"决定:KMP/CMP 库名头不能自动推出"加 ohos target"——compose-rules 顶着 Compose 的名字,却没有一行代码上设备;判据是依赖形态(JVM-only)与执行环境(构建期),kotlinx-benchmark 的 plugin 面早已给出先例,本次是把这条路线走到底;
- 构建期工具的"换线"要换到解析器本体:只升 Gradle 插件不升
kotlin-compiler-embeddable,lint 引擎还是旧语言版本;版本目录一处kotlin = 2.2.21-1.0.0让构建与解析同时换线,再用 190 项测试证明运行时兼容,这才是"鸿蒙线 lint"的完整含义; - 验收要证明"真的咬合",不要演示"看起来在跑":lint-demo 的门禁(每引擎 ≥10 条违规、≥6 条规则)+ 双引擎计数对称,比任何截图都有说服力;HAP 报告页展示构建期真实数据(工具链指纹来自构建环境注入),占位数据明确标黄——静态工具的验收诚实度全靠这些细节。
9.4 适配成果
当前适配已完成:
- compose-rules
0.0.26-ohos.1在鸿蒙工具链线上的完整构建(三文件五调用点 API 迁移,17 条规则逻辑零改动); - HarmonyOS Kotlin 制品线在 lint 工具库上的落地(Kotlin
2.2.21-1.0.0全家桶,Gradle 8.14.1 + JDK 21); - 190 项上游测试全绿(测试文件零改动)+ lint-demo 双引擎验收(24 + 24 对称、14 条规则命中、门禁通过);
- DevEco Stage ArkUI 三 Tab 验收页面(规则目录、真实扫描报告、适配说明);
- HAP 构建、签名、设备安装和真机验证(纯 ArkTS,无 native 依赖);
- AtomGit 项目文档、OpenHarmony README(中英双份)、适配全流程博客和真机效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐


所有评论(0)