本文记录 Thistle 接入 OpenHarmony 的完整过程,覆盖富文本标记库盘点、解析器依赖 kudzu 的鸿蒙制品缺失应对、真实解析器编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI Text + Span 真机页面、HAP 构建和真机验收。

Thistle 是 copper-leaf 出品的 Kotlin Multiplatform 字符串标记库:业务方写一段带标记的普通字符串({{b}}bold{{/b}}、{{foreground color=red}}red{{/foreground}}),它用 PEG 组合子解析器(kudzu)解析成平台无关的 AST,再交给各平台的渲染器,变成 Android Spannable、Compose AnnotatedString 或控制台 ANSI 转义码。本次适配与社区两条既有路线都有区别:既不是 qrose / haze / PreCompose / voyager 的"零依赖内核提取",也不完全等同于 KStore 的"真编译整库 + 补制品线"——Thistle 的解析引擎 kudzu 在社区 Maven 上没有任何 ohos 制品(上游只发布 JVM jar,没有 klib),因此采用源码 vendor 进仓:kudzu-core 4.0.0 的 129 个 commonMain 文件原样入仓编成 OpenHarmony KLIB,上游 thistle-core / thistle-console 的源码零改动;Compose 渲染器(thistle-compose-ui)因 Compose 插件 1.2.0 / Kotlin 1.7.20 与鸿蒙工具链不对齐而隔离在构建之外,由 OhosDefaults 在桥里复刻 ComposeDefaults 的 13 个标签语义,ArkUI 页面用原生 Text + Span 逐段着色作为验收宿主。

项目地址: AtomGit/oh-tpc/thistle.git

上游仓库: copper-leaf/thistle

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

Thistle 为 Kotlin Multiplatform 应用提供"一份标记,多端渲染"这一件事的完整答案:ThistleParser(AndroidDefaults(context)) 一行建解析器,{{tag attr=value}} 语法覆盖粗斜体、下划线删除线、前景背景色、字体族、上下标,{name} 插值把动态值从 context 表带进文案,未声明标签直接报错而不是静默吞掉。解析与渲染严格分层:thistle-core 只负责"标记字符串 → AST",AndroidDefaults / ComposeDefaults / ConsoleDefaults 都是往 ThistleSyntaxBuilder 里注册标签工厂、再配一个 ThistleRendererFactory 的插件。KMP 项目把这些能力放在 commonMain,由 Android、桌面共享。OpenHarmony 应用同样需要这套富文本标记能力——服务端下发富文本、跨端一致的文案样式、新手引导高亮都是真实场景,但上游构建(Kotlin 1.7.20 / Gradle 7.6 / buildSrc 约定插件)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示文字,却无法证明公共解析器在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证解析器引擎、解析器依赖(kudzu)、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 1.7.20 / Gradle 7.6 / buildSrc 约定插件(AGP 7.2、Compose 插件 1.2.0)与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵thistle-core 的每一个解析入口都长在 kudzu 的组合子上,而 Maven Central 的 kudzu-core:4.0.0 只有 JVM jar——jar 里是 .class 字节码,Kotlin/Native 的 ohos 目标消费不了。
渲染语义跨不了桥thistle-compose-ui 的产物是 Compose AnnotatedString,依赖 androidx.compose.ui.text,鸿蒙侧没有对应制品;上游 Compose 插件 1.2.0 与社区 CMP 1.9.2 两条版本轴对不齐。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:真实解析器与 ANSI 渲染器的 ohos 编译、kudzu 源码 vendor、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。解析语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和用 Text + Span 展示结果。

1.2 库提供的能力

Demo 提供一个真实的标记解析会话,对应桥接的全部入口:

  • 标记渲染:一段文本里混排 {{b}} / {{i}} / {{u}} / {{strikethrough}} / {{foreground color=...}} / {{background color=...}} / {{style style=...}} / {{typeface typeface=...}} / {{monospace}} / {{sans}} / {{serif}} / {{superscript}} / {{subscript}} 十三个标签,解析渲染成样式化分段;
  • 插值:{name}(单花括号)从请求的 context 对象取值,数字、布尔、带空格逗号的字符串都按上游语义 toString 替换;
  • 三种颜色写法:11 个颜色名(red / green / #rrggbb / #aarrggbb 大小写不敏感)与上游 Color.Companion.parse 同一张表;color=#rrggbb 被上游解析器转成 ARGB Int,桥再序列化回颜色串;
  • 嵌套叠加:{{foreground color=blue}}{{b}}bold blue{{/b}}{{/foreground}} 样式逐层合并,分段在样式变化处切分;
  • 错误通道:未闭合标签、未声明标签、缺失插值键全部走同一个 ok:false 通道带回上游异常原文,不崩、不跨语言抛异常。

响应是一个扁平的分段数组:

字段作用
ok解析渲染是否成功,失败时 error 带上游异常原文。
spans样式化分段数组,每段必有 text;样式字段全可选,无样式的段只有一个 text。
样式字段fontColor / backgroundColor / fontWeight / fontStyle / fontFamily / textDecoration / baselineShift / fontSize 八个可选键,与 ArkUI Span 属性几乎一一对应。

解析器侧完整能力(kudzu 的嵌套标签、属性表、错误恢复;core 的标签动态校验、值解析器族)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 15 项桥协议冒烟测试交叉验证,证明 ohos .so 里执行的解析逻辑没有漂移。

1.3 实现适配

维度要求
代码复用thistle-core 与 thistle-console 的全部 commonMain 文件原样编译,不提取、不重写;kudzu-core 4.0.0 源码原样 vendor,仅新增 ohosMain / jvmMain 各一个 KudzuPlatform actual。
平台目标根构建直接声明 ohosArm64 + ohosX64,三个库模块保留 jvm() 作交叉验证,examples/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);thistle-compose-ui、thistle-android、docs 留盘不进构建;绑死旧工具链的 buildSrc 与上游 CI 整体删除。
桥接稳定使用 1 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律thistle-core 对外的唯一依赖从 Maven 坐标换成 project(":kudzu-core"),kudzu 源码与上游 4.0.0 逐字一致。
UI 完整三页签覆盖全部桥接入口:渲染(标记输入 + context + 结果区)、标签速查(13 个标签逐一自证)、JSON(原始请求 / 响应检视)。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Thistle 上游的 Compose 渲染器隔离在构建之外,ArkTS Demo 用的是鸿蒙原生 Text + Span 组件——这正是 thistle 的天然优势:它本来就把解析与渲染分层,桥只需要复刻 SpanStyle 的字段语义,不需要复刻 Painter。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 kudzu 无 ohos 制品、确定交付物
第 2 阶段:工具链与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、kudzu 源码 vendor
第 3 阶段:桥协议建模     ── 标记文本 + context 载荷、spans JSON 契约、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 三页签、Text+Span 逐段渲染、标签速查自证、JSON 检视
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
kudzu-core/                           vendor 进仓的 kudzu-core 4.0.0(129 个 commonMain 文件 + ohos/jvm 两个 actual,进 ohos 构建)
thistle-core/                         上游解析引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64,源码零改动)
thistle-console/                      上游 ANSI 渲染器(进 ohos 构建,commonMain 原样,源码零改动)
thistle-compose-ui/                   上游 Compose AnnotatedString 渲染器,留盘不进构建
thistle-android/                      上游 Spannable 渲染器,留盘不进构建
examples/nativeApp/                   Kotlin/Native 动态库(ThistleBridge + Bridge + MiniJson + JVM 冒烟测试)
examples/harmonyApp/                  DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
docs/ examples/android 等             上游文档站与旧示例,留盘不进构建

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :kudzu-core、:thistle-core、:thistle-console 与 :examples:nativeApp,rootProject.name = "ohos_thistle"。上游的 buildSrc 约定插件(版本管理 / 发布 / lint / testing)、binary-compatibility-validator、Orchid 文档站、GitHub Actions 发布链全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin jdk-21)
OpenHarmony 目标ohosArm64 + ohosX64(三个库模块 jvm() 保留作交叉验证)
Gradle8.14.1(2.2.21 线要求 8.x,wrapper 升级自上游 7.6)
kudzu4.0.0 源码 vendor 进仓(无制品线可换——这是与 kotlinx 系仓库的本质差异)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.thistle
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
kudzu-core129纯 Kotlin,唯一 expect 是 KudzuPlatform(isLetter / isDigit / isLetterOrDigit)vendor 整体编译(jvm + 双 ohos)
thistle-core15纯 Kotlin + kudzu,零 UI、零 AGP、零 expect整体编译(源码零改动)
thistle-console12纯 Kotlin + thistle-core,ANSI SGR 码表整体编译(源码零改动)
thistle-compose-ui16androidx.compose.ui.text 全家桶留盘不进构建
thistle-android20Android Spannable留盘不进构建

expect 声明全链路只有 1 处:kudzu 的 public expect object KudzuPlatform。上游给了 jvm / ios / js 三个 actual,ohos fork 补两个——ohosMain 与 jvmMain 各一份,实现与上游 jvm actual 同款(stdlib 的 Char.isLetter() 系列在 Native 上原生可用),这就是整个 fork 在 vendor 模块里仅有的新增源文件:

// kudzu-core/src/ohosMain/kotlin/com/copperleaf/kudzu/KudzuPlatform.kt
public actual object KudzuPlatform {
    public actual fun isLetter(char: Char): Boolean = char.isLetter()
    public actual fun isDigit(char: Char): Boolean = char.isDigit()
    public actual fun isLetterOrDigit(char: Char): Boolean = char.isLetterOrDigit()
}

thistle-core 值得单独说明:它对外的唯一依赖就是 io.github.copper-leaf:kudzu-core:4.0.0,SequenceParser、PredictionParser、IdentifierTokenParser、TagParser……每一个解析入口都长在 kudzu 的组合子上。没有 kudzu 就没有 thistle,所以 vendor kudzu 是本次适配的成败手,处置见第 2.1 节。

1.4 页面边界
标题区          thistle · OpenHarmony / 副题行(富文本标记库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        渲染 / 标签速查 / JSON
标记输入区      thistle 标记多行文本域(默认文案覆盖全部 13 个标签)
context 区      context JSON 文本域(默认 {"name":"HarmonyOS"},可留空)
操作区          渲染按钮
结果区          Text + ForEach(Span) 逐段着色;失败时红色错误文本
标签速查区      13 个标签的名称、参数说明、示例源码 + 实时桥渲染自证
JSON 区         最近一次请求 / 响应的原始 JSON(等宽字体滚动区)

页面启动时把 13 个标签的示例逐个送过真实桥做自证渲染,再对默认文案做一次完整渲染。

第 2 阶段:工具链与依赖打通

2.1 kudzu 制品缺失的应对(本适配的成败手)

适配 thistle 与适配 kotlinx 系仓库的第一步就分岔了。KStore 一类仓库可以先查社区 Maven 上的 ohos 制品线;thistle 的依赖 kudzu 是 copper-leaf 自家的小众解析器组合子库,社区 Maven 和 Maven Central 上都只有 JVM jar:

io.github.copper-leaf:kudzu-core:4.0.0
  artifacts: kudzu-core-4.0.0.jar / -all.jar / -sources.jar   ← 无任何 .klib

jar 里是 .class 字节码,ohosArm64 / ohosX64 目标配置期就会报「依赖没有 ohos 变体」。三条路里选了 vendor:换解析器是白做(解析器就是这类库的全部价值);手写 ohos 变体发布等于替上游维护 fork 制品;把 4.0.0 的 sources jar 原样解包进仓是最诚实的方式——commonMain 的 129 个文件逐字节与上游一致(kudzu 本身是纯 Kotlin,无任何平台 API),只补 ohosMain / jvmMain 两个 actual,thistle-core 的依赖从 Maven 坐标换成 project(":kudzu-core")。vendor 的出处、license(与 thistle 同为 BSD-3、同一位上游作者)和做法都写进了 kudzu-core/README.md。

2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

社区已审仓库(qrose / haze)的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1,本仓沿用。两点说明:其一,上游 thistle 本来就是 Kotlin 1.7.20 编的,2.2.21 读 1.7 元数据毫无压力,kudzu 4.0.0 同理;其二,Gradle 经验必须按工具链线区分——KStore 适配时 Kotlin 2.4.20-0.1.0-07 线可以沿用 Gradle 9.x,而 2.2.21-1.0.0 旧线必须配 Gradle 8.x(本仓 wrapper 从上游 7.6 升到 8.14.1),两条线的经验不能互相套用。

settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_thistle"

include(":kudzu-core")
include(":thistle-core")
include(":thistle-console")
include(":examples:nativeApp")
2.3 编译真实解析器

三个库模块的构建文件收敛为同一形状(以 thistle-core 为例):

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()                // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {
            api(project(":kudzu-core"))   // 上游是 Maven 坐标,fork 换 project 依赖
        }
    }

    // 上游 kotest 套件保留源码,fork 不跑(见 2.4)
    tasks.matching { it.name.contains("Test") }.configureEach { enabled = false }
}

实测:

./gradlew :kudzu-core:compileKotlinOhosArm64 :kudzu-core:compileKotlinOhosX64     # BUILD SUCCESSFUL
./gradlew :thistle-core:compileKotlinOhosArm64 :thistle-core:compileKotlinOhosX64 # BUILD SUCCESSFUL
./gradlew :thistle-console:compileKotlinOhosArm64 :thistle-console:compileKotlinOhosX64 # BUILD SUCCESSFUL

上游 thistle-core / thistle-console 的全部 commonMain 文件一个没动,kudzu 只有两个 actual 是新的。

2.4 测试依赖的处置:kotest 与 Kotlin 2.2.21 未对齐

上游测试用 kotest 5.5.1(engine / assertions / datatest / property 四件),构建于 Kotlin 1.7 时代,测试框架与 Kotlin 2.2.21 工具链的适配没有验证过。处置是两条纪律的组合:上游测试文件零改动保留在 commonTest,各模块显式禁用全部 Test 任务,根构建再兜一层:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

解析正确性改由 examples/nativeApp 的 JVM 冒烟测试兜底(见第六节)——它跑的不是上游测试,而是桥协议本身,但跑在与 ohos 完全相同的 commonMain 代码上。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.copperleaf.thistle.core/**(上游解析引擎,原样编译)
        ↓ ThistleParser(OhosDefaults()) / kudzu PEG → AST
ThistleBridge.kt(commonMain:OhosDefaults 渲染 + MiniJson 序列化)
        ↓ UTF-8 JSON
C ABI ThistleRender(const char*) → N-API thistleRender(string)
        ↓
ArkUI Index.ets(三页签)

请求与响应:

请求 : {"text":"Hello {name}! {{b}}bold{{/b}}","context":{"name":"HarmonyOS"}}
响应 : {"ok":true,"spans":[
         {"text":"Hello "},
         {"text":"HarmonyOS"},
         {"text":"! "},
         {"text":"bold","fontWeight":"bold"}
       ],"error":null}
失败 : {"ok":false,"spans":[],"error":"Unknown tag: boldx. Valid tag names: [foreground, ...]"}

三个不对称设计:spans 是扁平分段数组而不是样式区间树——ArkUI 的 Span 是"文本段 + 属性"模型,扁平分段 ForEach 一把就能渲染;分段粒度是样式变化处,相同样式的连续文本合成一段;样式字段全可选,无样式的段只有一个 text,ArkTS 侧逐字段判空。

3.2 OhosDefaults:为什么镜像 ComposeDefaults

上游渲染器是插件式的:ThistleSyntaxBuilder.Defaults 接口 = 标签注册表 + 渲染器工厂。鸿蒙桥实现了一个 OhosDefaults,按上游 ComposeDefaults 的清单一比一注册 13 个标签——名称、参数名、取值域全部照抄,产物不是 SpanStyle 而是平台无关的 SpanSegment:

public class OhosDefaults : ThistleSyntaxBuilder.Defaults<OhosThistleRenderContext, SpanStyleModel, List<SpanSegment>> {
    override fun applyToBuilder(builder: ThistleSyntaxBuilder<OhosThistleRenderContext, SpanStyleModel, List<SpanSegment>>) {
        with(builder) {
            tag("foreground") { OhosForegroundColor() }
            tag("background") { OhosBackgroundColor() }
            tag("style") { OhosStyle() }
            tag("typeface") { OhosFontFamily() }
            tag("monospace") { OhosFontFamily("monospace") }
            tag("sans") { OhosFontFamily("sans") }
            tag("serif") { OhosFontFamily("serif") }
            tag("strikethrough") { OhosTextDecoration("lineThrough") }
            tag("subscript") { OhosBaselineShift("subscript") }
            tag("superscript") { OhosBaselineShift("superscript") }
            tag("b") { OhosStyle(hardcodedWeight = "bold") }
            tag("i") { OhosStyle(hardcodedFontStyle = "italic") }
            tag("u") { OhosTextDecoration("underline") }
        }
    }
}

每个标签工厂都用上游的 checkArgs + parameter / enum 委托实现,参数名与上游逐字一致(color=、style=、typeface=),连上游 ComposeTextDecoration 参数映射表里 "underline" 也指向 LineThrough 的历史笔误都原样镜像——尽管 13 个默认标签全部走硬编码路径、永远碰不到那张表,镜像它保证了"语义对齐上游"这句话经得起逐行核对。

颜色语义同样照抄上游 Color.kt:颜色名 11 个 + #rrggbb / #aarrggbb;color=#ffcc00 会被上游的 hexColorAsIntValueParser 转成置了 alpha 的 ARGB Int,桥序列化时把全不透明的折回 #rrggbb 短形式(与输入写法一致),带透明的才输出 #aarrggbb;未知颜色降级为无样式(对齐上游 Color.Unspecified),不报错。

3.3 错误通道

导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("ThistleRender")
fun thistleRender(request: String): String = try {
    /* 解析 + 渲染 + 序列化 */
} catch (t: Throwable) {
    """{"ok":false,"spans":[],"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、解析错误(Parse error at 1:18 ...)、未声明标签(Unknown tag: ...)、缺失插值键(Context must contain value for key '...'),全部走同一个 ok:false 通道,ArkTS 侧统一渲染。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libthistle_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosthistle.so)
  │ ThistleRender
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复刻解析语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("ThistleRender")
fun thistleRender(request: String): String   // 一口进:标记 + context → spans JSON
C ABI 符号ArkTS 方法覆盖内核
ThistleRenderthistleRender(req)ThistleParser(OhosDefaults()) 构造 + kudzu 解析 + 13 标签渲染 + JSON 序列化

桥的全部逻辑放在 examples/nativeApp/src/commonMain(ThistleBridge.kt),ohosMain 只留 @CName 一层薄壳——这是刻意安排:jvmSmokeTest 在 JVM 上跑的与设备上是同一份代码。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"thistleRender", nullptr, NapiThistleRender, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosthistle_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosthistle_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),并与 KN 生成头(build/bin/<abi>/debugShared/libohosthistle_api.h)逐符号核对后才提交。

4.4 CMake 和类型声明
add_library(ohosthistle SHARED IMPORTED)
set_target_properties(ohosthistle PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(thistle_napi SHARED napi_init.cpp)
target_link_libraries(thistle_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosthistle)

OHOS_ARCH 为空或 libohosthistle.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。KN so 本体由 examples/nativeApp 的 linkDebugSharedOhosArm64 / linkDebugSharedOhosX64 链出,copyKnSo* 任务自动把 so 与匹配 triple 的 libc++_shared.so 回填进 entry/libs/<abi>/。

export const thistleRender: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 触发渲染和刷新结果
import { thistleRender } from 'libthistle_napi.so';
import { LengthMetrics } from '@kit.ArkUI';

private renderCore(markup: string, contextJson: string): RenderResponse {
  const req: Record<string, Object> = { 'text': markup };
  const ctx = contextJson.trim();
  if (ctx.length > 0) {
    req['context'] = JSON.parse(ctx) as Record<string, Object>;   // 非法 JSON 本地拦截,不发 Native 调用
  }
  const raw = thistleRender(JSON.stringify(req));
  return JSON.parse(raw) as RenderResponse;
}

结果区把 spans 摊进一个 Text,逐段条件链式上样式;ok:false 时错误原文红色显示,全程 try/catch 不崩:

Text() {
  ForEach(spans, (s: SpanStyle) => {
    Span(s.text)
      .fontColor(s.fontColor ?? '#111827')
      .backgroundColor(s.backgroundColor ?? Color.Transparent)
      .fontWeight(s.fontWeight === 'bold' ? FontWeight.Bold : FontWeight.Normal)
      .fontStyle(s.fontStyle === 'italic' ? FontStyle.Italic : FontStyle.Normal)
      .fontFamily(this.mapFontFamily(s.fontFamily))
      .decoration({ type: /* underline / lineThrough / none */ })
      .baselineOffset(LengthMetrics.fp(/* superscript +8 / subscript -4 / 0 */))
      .fontSize(s.fontSize ?? 16)
  }, (s: SpanStyle) => JSON.stringify(s))
}
5.2 页面交互

渲染 Tab 自上而下:标记输入域(默认文案一次覆盖全部 13 个标签)、context JSON 输入域、渲染按钮、结果卡片与状态行(渲染成功 · 28 段)。标签速查 Tab 用静态数组列出 13 个标签的名称、参数说明与示例源码,aboutToAppear 时把每条示例真实送过桥渲染出 Span 自证——页面自己就是 13 个标签的验收用例。JSON Tab 用等宽字体滚动区展示最近一次请求 / 响应的原始 JSON,桥协议一眼可查。

5.3 对比度和错误边界

ok:false 时错误原文红色进结果区,context JSON 非法时本地报错、不发起 Native 调用;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进错误显示。

第 6 阶段:示例与验证

6.1 工程结构
examples/
├── nativeApp/
│   ├── build.gradle.kts                              jvm() + 双 ABI sharedLib(baseName=ohosthistle)+ cxxShared + copyKnSo* + jvmSmokeTest
│   ├── src/commonMain/kotlin/example/nativeapp/
│   │   └── ThistleBridge.kt                          OhosDefaults + 13 标签工厂 + OhosThistleRenderer + MiniJson + JSON 序列化
│   ├── src/ohosMain/kotlin/example/nativeapp/
│   │   └── Bridge.kt                                 @CName("ThistleRender") 薄壳
│   └── src/jvmMain/kotlin/example/nativeapp/
│       └── Main.kt                                   桥协议冒烟测试(15 项)
└── harmonyApp/
    ├── build-profile.json5                           signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                            bundleName org.terminator.ohos.thistle、versionName 4.0.1-ohos.1
    └── entry/
        ├── build-profile.json5                       externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/                  linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt               IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp                N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohosthistle_api.h
        ├── src/main/cpp/types/libthistle_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets              ArkUI 验收页面(三页签)
6.2 构建、签名和安装
./gradlew :examples:nativeApp:jvmSmokeTest                                        # 15 项桥协议冒烟
./gradlew :examples:nativeApp:linkDebugSharedOhosArm64 :examples:nativeApp:linkDebugSharedOhosX64

# DevEco Studio 打开 examples/harmonyApp(不要打开仓库根目录)
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填完整配置)。

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.thistle -m entry

四、完整代码对照

4.1 整体架构

com.copperleaf.kudzu/**(上游 4.0.0 源码 vendor,原样编译)
    │ PEG 组合子:Sequence / Choice / Tag / Value Parser 族
com.copperleaf.thistle.core/**(上游原样编译)
    │ ThistleParser / ThistleSyntaxBuilder / 标签动态校验 / 值解析器族
com.copperleaf.thistle.console/**(上游原样编译)
    │ ConsoleDefaults + SGR 码表(不经桥,鸿蒙侧随编译可用)
ThistleBridge.kt(commonMain)
    │ OhosDefaults 13 标签 / SpanSegment 模型 / MiniJson
libthistle_napi.so
    │ thistleRender(req)
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(Text + Span 逐段着色)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_thistle、四个模块
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 插件钉版
build.gradle.kts(根)版本 4.0.1-ohos.1、全局禁用 Ohos*Test* 任务
kudzu-core/vendor kudzu-core 4.0.0(129 文件 + ohos/jvm 两个 actual)
thistle-core / thistle-console / kudzu-core 的 build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,禁 Test 任务
examples/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填、jvmSmokeTest
nativeApp/.../ThistleBridge.ktOhosDefaults、SpanSegment 渲染器、MiniJson、错误通道
nativeApp/.../Bridge.kt@CName("ThistleRender") 薄壳
nativeApp/.../Main.ktJVM 桥协议冒烟测试(15 项)
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(三页签)
README.OpenHarmony_CN.md鸿蒙适配主文档(17 节)

4.3 关键 API 对照

层次API作用
解析器ThistleParser(OhosDefaults())建解析器:标签注册表 + PEG 语法一次成型
解析器thistle.parse(ParserContext.fromString(text))标记串 → AST,未声明标签在此抛 ThistleUnknownTagException
渲染器thistle.newRenderer().render(rootNode, context)AST + context → List<SpanSegment>
标签foreground / background / style / typeface / monospace / sans / serif / strikethrough / subscript / superscript / b / i / u13 个标签,参数与语义镜像上游 ComposeDefaults
插值{name}单花括号,从 context 取值 toString 替换
NativeThistleRenderC ABI JSON 导出
N-APIthistleRender(req)向 ArkTS 暴露能力
ArkTSrenderCore(markup, context)触发解析并刷新 Text + Span

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(thistleRender(JSON.stringify({
  text: 'Hi {name}, {{b}}bold{{/b}} and {{foreground color=red}}red{{/foreground}}',
  context: { name: 'ohos' }
})));
// res.ok === true;res.spans 逐段带 text + 可选样式字段
val thistle = ThistleParser(OhosDefaults())
val rootNode = thistle.parse(ParserContext.fromString(text))
val spans = thistle.newRenderer().render(rootNode, context)   // List<SpanSegment>

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:kudzu 源码 vendor 进仓,而不是换依赖或重写解析器

thistle 的全部价值就在 kudzu 组合子搭出来的解析器里。社区 Maven 没有 kudzu 的 ohos 制品(连 klib 都没有),换线不可行;在 ArkTS 里重写解析器必出分叉;替上游维护一份 ohos fork 制品又超出单个库适配的范围。sources jar 原样解包进仓是最诚实的方式:129 个 commonMain 文件逐字与上游 4.0.0 一致、license 同为 BSD-3、只补两个 KudzuPlatform actual,出处写进模块 README 供审阅。

决策 2:工具链钉 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1

沿用社区已审钉法(qrose / haze 同款)。上游 Kotlin 1.7.20 的源码与 kudzu 4.0.0 的元数据在 2.2.21 下零兼容成本;Gradle 必须配 8.x——KStore 适配的「2.4.20 线沿用 Gradle 9.x」经验在这条旧线上不成立,工具链线的经验不能互相套用。

决策 3:根构建替换(cmp_qrose 模式),compose-ui / android 留盘

上游根构建(buildSrc 约定插件、binary-compatibility-validator、Orchid 文档站、GitHub Actions)对鸿蒙没有保留价值,整体替换为极简 ohos 构建;buildSrc 绑死 Kotlin 1.7.20 / AGP 7.2,Gradle 每次构建都会先编它,直接删除。thistle-compose-ui 留盘有三个理由:对照 ComposeDefaults 写 OhosDefaults 时不用翻上游仓库;等社区 CMP 工具链与上游版本对齐后改回 source set 就能接;diff 历史保持完整。留盘不编和删掉是两回事。

决策 4:OhosDefaults 镜像 ComposeDefaults,ArkUI 原生渲染而不是光栅化

qrose 光栅化是因为条码的终点本来就是一张位图;文本的终点就是文本组件。光栅化文本会同时失去可选、可缩放、无级字体、系统无障碍四样东西,等于在 KN 里重写一遍文本排版。ArkUI Span 原生就有 fontColor / fontWeight / fontFamily / decoration / baselineOffset,和 SpanStyle 字段几乎一一对应——桥只复刻 SpanStyle 的字段语义,不复刻 Painter。13 个标签的名称、参数、取值域与上游逐字对齐,验收时同一标记串可以拿上游 Compose / Android 渲染结果逐段比对。

决策 5:桥逻辑进 commonMain + JVM 冒烟测试

ThistleBridge.kt 放 commonMain,ohosMain 只留 @CName 壳,nativeApp 同时开 jvm() 目标挂 jvmSmokeTest——JVM 与 ohos 编译同一份桥代码,15 项冒烟用例(插值、13 标签、三种颜色写法、嵌套叠加、错误通道)在 JVM 上验证过的行为就是设备上会跑的行为。上游 kotest 与新工具链未对齐不能跑,交叉验证的闸门就移到桥协议层:段落边界、每段样式字段值都钉死在用例里。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Temurin)、DevEco Studio 5.1.0 Release(HarmonyOS SDK 5.1.0(18))、hvigor / hdc 命令行与鸿蒙真机(arm64)。

6.2 JVM 桥协议冒烟验证

./gradlew :examples:nativeApp:jvmSmokeTest

15 项用例全部通过(跑在与 ohos 相同的 commonMain 桥代码上):

用例组钉住什么
插值{name} 从 context 取值;数字、布尔 context 值按 toString 替换
{{b}} / {{i}} / {{u}} / {{strikethrough}}硬编码速记标签的 fontWeight / fontStyle / textDecoration
{{style style=bold / style=italic}}参数化样式标签(上游只收一个 style= 参数)
颜色三写法颜色名 → #rrggbb;#rrggbb 经 ARGB Int 折回短形式;#aarrggbb 保留
未知颜色降级为无样式(对齐上游 Color.Unspecified),不报错
{{superscript}} / {{subscript}}baselineShift + 附带 fontSize 缩小
字体族monospace / sans / serif 硬编码 + typeface=cursive 参数化
嵌套叠加{{foreground color=blue}}{{b}}…{{/b}}{{/foreground}} 样式合并、分段切分
参数化装饰映射上游 ComposeTextDecoration 映射表的历史行为原样镜像
错误通道未闭合标签 / 未声明标签 / 缺失插值键全部 ok:false 不崩
空文本回退默认演示文案并补默认 context

6.3 HAP 验证

:kudzu-core / :thistle-core / :thistle-console compileKotlinJvm: BUILD SUCCESSFUL
:kudzu-core / :thistle-core / :thistle-console compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 自动回填 entry/libs/<abi>/ 三件套)
生成头 libohosthistle_api.h 与仓内手写头逐符号核对:ThistleRender / libohosthistle_symbols / DisposeString 一致
CompileArkTS: finished(修复 baselineOffset 的 LengthMetrics 签名后)
PackageHap: finished
install bundle successfully
start ability successfully

HAP 内含双 ABI 各三件套(libthistle_napi.so / libohosthistle.so / libc++_shared.so);KN so 为 debugShared 未 strip 版本,含 Kotlin/Native 运行时,release 打包后显著缩小。

6.4 功能用例

  1. 启动后渲染 Tab 自动对默认文案执行一次完整解析渲染,状态行显示 渲染成功 · 28 段;
  2. 默认文案混排全部 13 个标签,结果卡片里粗体、斜体、下划线、删除线、红色前景、#ffcc00 背景、上标、下标、等宽、style=bold、衬线/无衬线同屏可见(见第七节效果图);
  3. {name} 插值取自 context JSON({"name":"HarmonyOS"}),渲染为 Hello HarmonyOS!;
  4. 标签速查 Tab 对 13 个标签逐条展示参数说明与示例源码,并用真实桥调用渲染出自证结果;
  5. JSON Tab 可见最近一次请求 / 响应的原始 JSON,与 JVM 冒烟用例的断言口径一致;
  6. 篡改 context 为非法 JSON,被前置校验本地拦截,不产生 Native 调用;
  7. 输入未闭合 / 未声明标签(如 {{b}}unclosed、{{nosuchtag}}),结果区红色显示上游异常原文,应用不崩;
  8. 清空文本再渲染,回退到内置演示文案——桥的兜底语义生效。

6.5 验证结论

解析器引擎(kudzu + thistle-core)、ANSI 渲染器(thistle-console)、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的解析会话:13 个标签、插值、三种颜色写法、嵌套叠加与错误通道(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/thistle.git
cd thistle
./gradlew :thistle-core:compileKotlinOhosArm64 :thistle-console:compileKotlinOhosArm64   # klib
./gradlew :examples:nativeApp:jvmSmokeTest                                               # 15 项桥协议冒烟
./gradlew :examples:nativeApp:linkDebugSharedOhosArm64 :examples:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 examples/harmonyApp(不要打开仓库根目录),勾选自动签名
cd examples/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.thistle -m entry
hdc shell hilog | grep Thistle

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :kudzu-core / :thistle-core / :thistle-console 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. kudzu-core:4.0.0 在 Maven Central 上只有 JVM jar,连 klib 都没有——「依赖没有 ohos 变体」报的是配置期,vendor 源码是唯一诚实解;
  4. expect/actual 的 actual 按目标补齐:kudzu 的 KudzuPlatform 只补 ohos actual 不够,fork 同时保留 jvm() 目标,jvmMain 的 actual 缺一个编译就报 Expected KudzuPlatform has no actual declaration in module <commonMain> for JVM;
  5. include 了的模块必须有自己的 build.gradle.kts——漏建的模块在 Gradle 眼里是空工程,消费方报 No matching variant ... No variants exist,报错信息完全指向依赖解析而不是漏文件,极易查错方向;
  6. thistle 的插值是单花括号 {name}(DefaultThistleSyntax 默认语法),写成 {{name}} 会被当成未声明标签报 Parse error ... Expected '{{/'——Demo 默认文案、文档、测试用例都按单花括号写;
  7. color=#ffcc00 会被上游 hexColorAsIntValueParser 转成置了 alpha 的 ARGB Int,桥序列化时全不透明的要折回 #rrggbb 短形式,否则响应里出现 #ffffcc00 这类冗余串;
  8. MiniJson 从扁平 map 取 text 时必须剥引号反转义,否则渲染出来的每段文本都带一对引号——这种错在编译期零提示,全靠冒烟测试钉住;
  9. 上游 {{style}} 标签只收一个 style= 参数(取值 bold / italic),没有 weight=——Demo 文案按想象写 weight=bold 会直接 ok:false;
  10. 上游测试统一 JVM 保留、模块级禁用 Test 任务 + 根构建禁用 Ohos*Test*:kotest 5.5.1 与 Kotlin 2.2.21 未对齐,测试框架的债不应阻塞 ohos 编译链接;
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  12. ArkTS 的 Span.baselineOffset 在 API 12+ 收 LengthMetrics,传裸 number 编译期直接报 10505001——用 LengthMetrics.fp(...) 包一层(@kit.ArkUI 导出);
  13. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();仓内手写的极简头文件要与 KN 生成头逐符号核对后再提交。

8.2 已知问题

  • thistle-compose-ui(Compose AnnotatedString 渲染器)与 thistle-android(Spannable 渲染器)源码留盘不进构建:上游 Compose 插件 1.2.0 / Kotlin 1.7.20 与社区 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21 两条版本轴对不齐;等工具链对齐后改回 source set 接回,OhosDefaults + JSON 桥降级为 fallback;
  • thistle-console 的 ANSI 渲染随编译进入 .so,但鸿蒙侧没有开箱即用的终端宿主,桥不输出 SGR 串——它的价值在「上游测试与解析语义对照」和未来终端类应用消费;
  • 上游 kotest 测试未跑(与 Kotlin 2.2.21 未对齐,源码零改动保留),解析正确性由 JVM 冒烟测试 + 真机逐段比对兜底,强度低于单测回归;
  • 上游 ComposeTextDecoration 参数映射表里 "underline" 也指向 LineThrough 的历史笔误被原样镜像;13 个默认标签全走硬编码路径不受影响,只有绕过默认标签集的参数化装饰才可能碰到;
  • 桥不暴露注册自定义标签的入口(core 的扩展点是开放的,但要业务方自己写标签工厂再编进 .so),鸿蒙侧当前交付的是上游 ComposeDefaults 的 13 个默认标签;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.thistle 匹配的签名。

8.3 未来优化方向

  • 社区 CMP 工具链与上游 Compose 版本对齐后,把 thistle-compose-ui 接回 ohos source set,JSON 桥降级为 fallback;
  • kotest 出与 Kotlin 2.x 对齐的版本后,恢复上游测试源集,把交叉验证升级回上游原版用例;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 把 jvmSmokeTest 的用例口径开放成 ArkTS 侧可执行的对账脚本(真机渲染结果与 JVM 断言自动比对);
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵。

九、总结

9.1 核心难点回顾

上游 thistle-core + thistle-console(全部 commonMain,零改动)
    → 盘点依赖:kudzu 无任何 ohos 制品(连 klib 都没有)
    → kudzu-core 4.0.0 源码 vendor 进仓 + 补 ohos/jvm 两个 actual
    → 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1,2.2.21 线专属)
    → 根构建替换(cmp_qrose 模式,compose-ui/android 留盘,buildSrc 删除)
    → ohosArm64 / ohosX64 KLIB
    → C ABI(@CName("ThistleRender"))+ spans JSON 契约
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析
    → ArkUI 真机三页签验收(13 标签 / 插值 / 三种颜色写法 / 错误通道)

9.2 封装层次

  • 解析层:上游原版 kudzu 4.0.0 组合子 + ThistleParser / ThistleSyntaxBuilder,标签动态校验与值解析器族原样;
  • 渲染层:OhosDefaults 镜像上游 ComposeDefaults 的 13 个标签语义,产物为平台无关的 SpanSegment;
  • Native 层:双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、context 校验、渲染分发、错误显示与三页签导航;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 适配路线跟着依赖的制品可达性走:kotlinx 系仓库可以「真编译整库 + 补制品线」,依赖小众库的仓库就得「源码 vendor + 补 actual」——路线没有优劣,只有当天的制品事实;vendor 的底线是逐字一致 + 出处可查 + 差异(actual)最少;
  2. 工具链经验按线区分、不能互套:2.2.21-1.0.0 线配 Gradle 8.x,2.4.20 线可留 Gradle 9.x;上游 Kotlin 版本与鸿蒙钉版的元数据兼容性,是决定兼容成本的第一变量;
  3. 桥逻辑进 commonMain 是 KMP 库适配的杠杆:JVM 冒烟测试跑的就是设备上跑的代码,上游测试跑不了时,把交叉验证的闸门移到桥协议层(段落边界 + 每段样式字段值)依然能钉住「内核没漂」。

9.4 适配成果

当前适配已完成:

  • Thistle 4.0.1(4.0.1-ohos.1)真实解析器的双 ABI Kotlin/Native 编译(上游 thistle-core / thistle-console 源码零改动,kudzu vendor 源码仅两个 actual);
  • kudzu-core 4.0.0 源码 vendor 进仓的完整落地(129 个 commonMain 文件 + ohos/jvm actual,BSD-3 出处可查);
  • 1 个 C ABI 符号和 C++ N-API 包装层(生成头与仓内头逐符号核对一致);
  • DevEco Stage ArkUI 三页签验收页面(渲染、标签速查、JSON 检视);
  • 15 项 JVM 桥协议冒烟全绿 + HAP 构建(双 ABI 三件套 so)、签名、设备安装和真机验证;
  • 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐