本文记录 carbon-compose 接入 OpenHarmony 的完整过程,覆盖纯 UI 型 CMP 库的资产盘点、设计 token 数据层抽取、真实注册表编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

carbon-compose 是 IBM Carbon Design System 的 Compose Multiplatform 非官方实现:按钮、标签、开关、通知、日期选择器、下拉等一整套组件,在 Android / iOS / Desktop / Wasm 上用同一套 Carbon 设计 token 着色与定尺。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")一脉相承但切入点不同:carbon-compose 的纯 Kotlin 资产不是算法,而是设计系统的数据模型——4 套主题、每套 299 个颜色 token、26 个字样、13 档间距、6 档动效时长与 6 条缓动曲线,全部来自上游 :code-gen 消费的 @carbon/themes 11.75.0 JSON。因此不需要重写组件、也不需要提取内核——真实的 token 数据被原样编译成 OpenHarmony KLIB 与双 ABI 动态库,一个完整的设计 token 引擎运行在真机上,ArkUI 目录页作为验收宿主;Compose 组件层原样留在磁盘上,等社区 CMP 鸿蒙线追上上游版本的那天再接回去。

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

上游仓库: gabrieldrn/carbon-compose

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

carbon-compose 为 Compose Multiplatform 应用提供"IBM Carbon 设计语言"这一件事的完整答案:20 多个组件、4 套颜色主题、成体系的排版与间距刻度,上游铁律是绝不引入 Material3——一切颜色、间距、字体都必须来自 token。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套设计语言,但上游构建(Kotlin 2.4.0 / Gradle 9.6.1 / AGP 9.2.1 / Compose Multiplatform 1.11.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS、色值手抄进页面,页面可以显示色块,却无法证明公共设计 token 引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证 token 数据层、序列化边界、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.4.0 / Gradle 9.6.1 / AGP 9.2.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建;约定插件 carbon.kmp.library 在配置期就会申请 AGP。
UI 层跨不过去组件层约 285 处 @Composable 全部站在 androidx.compose.* 上;社区鸿蒙 CMP 线是 1.9.2 / Kotlin 2.2.21,承载不了上游 Compose 1.11.1 的 API。
数据载体是 Compose 类型颜色 token 生成物 import androidx.compose.ui.graphics.Color,排版用 TextStyle,间距用 Dp——ohos 编译器看不到这些类型。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:设计 token 数据层的 ohos 编译(数据值逐字节来自上游 JSON)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。组件渲染语义由 ArkUI 验收页复刻,token 数据完全由上游数据源负责。

1.2 库提供的能力

Demo 提供一个真实的设计 token 会话,对应桥接的全部入口:

  • 元信息握手(meta):版本号、上游版本、数据源标注、四套主题名、各类 token 计数——全部来自真实 Native 调用而非静态文字;
  • 主题加载(theme):一套主题的全量颜色(192 核心 token + button / tag / notification / ai / chat 五个分组共 299 个,#AARRGGBB 字符串逐字节来自 @carbon/themes JSON)与深浅色标志;
  • 排版刻度(typography):26 个 Carbon type set 的家族 / 字号 / 行高 / 字重 / 字距;
  • 间距与动效(spacing / motion):13 档间距、6 档动效时长、6 条贝塞尔缓动曲线、6 档按钮高度。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op / name回显本次操作与目标主题(white / g10 / g90 / g100)。
colors 等原生 JSON 对象嵌入,ArkTS JSON.parse 一次到位,不做双重转义。
isDark主题深浅色标志,驱动页面整体换肤。

引擎侧完整能力(四套主题全部 299×4 = 1196 个颜色值、字样 / 间距 / 动效刻度)全部在注册表里随编译进入 .so,JVM 侧保留 8 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的数据没有漂移。

1.3 实现适配

维度要求
代码复用token 数据值逐字节来自上游 :code-gen 的输入 JSON(code-gen/src/main/resources/*.json,一字未动);注册表由可复现生成器产出,上游 Compose 模块零改动留盘。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);:carbon / :catalog 等上游模块留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律token 模块零第三方依赖(连 kotlinx 都不引),不存在制品线覆盖问题;桥沿用 cmp_qrose 审核通过的 MiniJson。
UI 完整单页覆盖全部桥接入口:主题切换、色板、排版、间距动效、组件预览、版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。carbon-compose 的组件层(285 处 @Composable)在当前鸿蒙工具链线上无法编译,适配面天然落在数据层;ArkTS Demo 是把桥暴露的 token 引擎画出来——每一个色块、每一行字的取色都来自 Native 调用返回的真实 token 值。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 token 数据源、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:token 注册表   ── 可复现生成器、纯 Kotlin 数据模型、零依赖 KMP 模块
第 4 阶段:零漂移闸门     ── jvmTest 与上游 JSON 逐值比对(1196 处颜色值)
第 5 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主     ── 主题切换、五 Tab 目录、组件复刻和真机验收

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
carbon-tokens/                        新增:纯 Kotlin token 注册表(进 ohos 构建,jvm + ohosArm64 + ohosX64)
  src/commonMain/.../CarbonTokenData.kt        生成的注册表(1433 行)
  src/commonTest/.../CarbonTokenDataValidationTest.kt  零漂移闸门
  src/commonTest/.../TestJsonParser.kt         测试专用迷你 JSON 解析器
  tools/generate-registry.ps1                  可复现生成器
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
code-gen/src/main/resources/*.json    上游数据源(原样保留,生成器与测试都读它)
carbon/ carbon-common/ carbon-test/   上游 Compose 库模块 —— 留盘,不进构建
catalog/ catalog-android/ iosCatalog/ 上游演示应用 —— 留盘,不进构建
build-logic/ buildSrc/ config/ docs/  上游构建设施 —— 留盘,不被引用
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :carbon-tokens 与 :example:nativeApp,rootProject.name = "ohos_carbon"。上游的 dokka / vanniktech 发布 / binaryCompatibilityValidator / AGP classpath / foojay 解析器全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin,org.gradle.java.home 写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(上游 9.6.1 wrapper 替换降级;2.2.21 线不支持 Gradle 9.x)
依赖carbon-tokens 零第三方依赖,无制品线要求
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.carbon
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块规模依赖形态处置
carbon218 个 .kt,285 处 @Composablecompose runtime / foundation / ui / animation / resources + kotlinx-datetime + kermit留盘不进构建(组件层,工具链对不齐)
carbon-common2 个 .ktcompose semantics留盘不进构建
carbon-test1 个 .ktcompose uiTest留盘不进构建
code-gen纯 JVM 生成器 + 4 份主题 JSONkotlinpoet / serialization代码留盘;JSON 成为新模块的数据源
doc-parserKotlin/JS@carbon/themes npm 包留盘不进构建(JSON 的上游产线,保持不动)
catalog / catalog-androidKMP 演示应用navigation / koin / settings留盘不进构建

expect/actual 盘点:全库仅 5 处(4 个尺寸枚举 + 1 个 rememberIsButtonPressed),平台耦合度极低——但它们是组件层的 expect,不影响 token 数据层;真正挡住 ohos 编译的是 Compose 运行时本身。

值得单独说明的是 foundation 层的资产形态:颜色主题(4 × 299 token)、排版刻度(26 个 type set)、间距(13 档)、动效(6 时长 + 6 缓动)、按钮高度(6 档)的值全部平台无关,只有载体类型(Color / TextStyle / Dp / CubicBezierEasing)是 Compose 的。这一刀切下去,适配面就从"285 个组件"缩小到"一张数据表"。

1.4 页面边界
标题区          CARBON DESIGN SYSTEM / carbon-compose · OpenHarmony 验收目录
主题区          White / Gray 10 / Gray 90 / Gray 100 四选一(切换即整页换肤)
Tab 栏          色板 / 排版 / 间距动效 / 组件 / 关于
色板页          13 个命名分区 + 其它,每 token 一个色块 + 名称 + 十六进制值
排版页          26 个 type set 按真实字号 / 行高 / 字重 / 字距渲染,附规格标注
间距动效页      13 档间距条、6 档时长、6 条缓动控制点、6 档按钮高度
组件页          按钮(6 变体 × 3 尺寸)、10 色标签、复选 / 单选 / 开关、进度条、4 态通知
状态行          最近一次操作结果(如"White 已加载 · 192 核心 token 来自 Kotlin/Native 注册表")
关于页          版本握手、数据源、token 计数、架构分层、验证链路、AtomGit 链接

页面启动时先调 carbonVersion() 做握手,再依次 dispatch meta / theme / spacing / typography / motion 拉全量数据。

第 2 阶段:目标与依赖打通

2.1 数据源验证(本适配的成败手)

KStore 适配先验证 kotlinx 制品线;carbon-compose 的对应动作是验证 token 数据源的唯一性与可达性。上游颜色 token 的产线是:doc-parser(Kotlin/JS)从 npm 包 @carbon/themes 11.75.0 提取 4 份 JSON → :code-gen(纯 JVM)在构建期把 JSON 生成 Compose Color 类。4 份 JSON 就在仓库里:

code-gen/src/main/resources/white.json   192 核心 token + 5 个组件分组
code-gen/src/main/resources/g10.json     结构与 white 完全一致
code-gen/src/main/resources/g90.json     深色主题
code-gen/src/main/resources/g100.json    深色主题

实测每份 JSON:192 个扁平颜色 token("background":"#FFFFFFFF" 这类)+ 5 个嵌套分组(buttonColors 15、tagColors 40、notificationColors 10、aiColors 21、chatColors 21),四套结构完全一致,合计每主题 299 个、四主题 1196 个颜色值。三个关键结论:其一,这是上游唯一的颜色数据源,直接消费它比手抄任何色表都更接近上游语义;其二,:code-gen 的生成物 import Compose 类型,ohos 编译器看不到,所以不能复用它的生成器,但可以复用它的输入;其三,JSON 里的值就是 #AARRGGBB 字符串,天然可跨语言,不需要任何转换。

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

社区批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。carbon-compose 上游是 Kotlin 2.4.0 源,但进 ohos 构建的新模块(carbon-tokens、example/nativeApp)只用了标准库语法,2.2.21 完全吃得下;上游源码本来就整体留盘不编译,不存在"读不了 2.4 元数据"的问题——这一点与 KStore 不同(KStore 的真实引擎依赖 coroutines 1.11.0-0.1.0-07,被 stdlib 元数据反推到 2.4.20-0.1.0-07)。2.2.21 线的代价是 Gradle 必须钉 8.14.1(不支持 9.x),上游 9.6.1 wrapper 整体替换。

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_carbon"

include(":carbon-tokens")
include(":example:nativeApp")
2.3 编译真实 token 注册表

carbon-tokens/build.gradle.kts 收敛为纯 KMP 模块:

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

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

    sourceSets {
        commonMain.dependencies {}          // 零依赖,标准库之外什么都没有
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

注册表本体 CarbonTokenData.kt(1433 行)由 tools/generate-registry.ps1 从 4 份 JSON 确定性生成:颜色值逐字节保留(键排序、值不转不换),spacing / typography / motion / 按钮高度从上游 foundation 源文件转录并写明出处。实测:

./gradlew :carbon-tokens:compileKotlinJvm :carbon-tokens:compileKotlinOhosArm64 :carbon-tokens:compileKotlinOhosX64   # BUILD SUCCESSFUL
2.4 测试的处置:注册表必须对照 JSON 逐值验证

数据表最容易出的问题不是编译失败,而是悄悄漂移——上游改了主题 JSON、注册表没跟上,页面照样能跑,颜色已经错了。处置是把"零漂移"做成测试(jvmTest),并沿用社区纪律:测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:

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

这样测试代码零妥协地保留在 commonTest,ohos 侧只承担编译与链接(详见 6.2)。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.gabrieldrn.carbon.tokens/**(纯 Kotlin 注册表)
        ↓  CarbonTokenData.themes / typeStyles / spacing / motion…
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API carbonDispatch(request) / carbonVersion()
        ↓
ArkUI Index.ets(五 Tab 目录页)

请求与响应:

请求 : {"op":"meta"}
       {"op":"theme","name":"white"}     // white / g10 / g90 / g100
       {"op":"spacing"} {"op":"typography"} {"op":"motion"}
响应 : {"ok":true,"op":"theme","name":"g90","isDark":true,
        "colors":{…192 项…},"buttons":{…},"tags":{…},
        "notifications":{…},"ai":{…},"chat":{…}}
       {"ok":false,"error":"…"}

两个不对称设计:请求是扁平键值对(桥内 MiniJson 70 行搞定,不引 kotlinx.serialization);响应是原生 JSON 嵌入——颜色值本来就是 #AARRGGBB 字符串,直接拼进响应对象,ArkTS JSON.parse 一次拿到 Record,读写两条路都不做双重转义。

3.2 载荷类型为什么固定 Map<String, String>

注册表不用任何业务模型包装颜色:token 名到十六进制字符串的映射原样暴露。这意味着桥不管 Carbon 的 token 语义——调用方(ArkTS)按 token 名取值、直接喂给 .backgroundColor() 这类接受字符串的 API;上游未来增删 token 名,桥零改动,注册表重生成即可。这等价于 KStore 的 T = JsonElement 决策:桥不绑定 schema,数据等价性交给源头与测试。

3.3 错误通道

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

@CName("CarbonDispatch")
fun carbonDispatch(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、不支持的 op、未知主题名,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。

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

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

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

4.2 Kotlin/Native 导出函数
@CName("CarbonDispatch")
fun carbonDispatch(request: String): String   // meta / theme / spacing / typography / motion 一口进

@CName("CarbonVersion")
fun carbonVersion(): String = CarbonTokenData.FORK_VERSION   // 版本握手
C ABI 符号ArkTS 方法覆盖内核
CarbonDispatchcarbonDispatch(req)全部 5 个 op:注册表查询与 JSON 序列化
CarbonVersioncarbonVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"carbonDispatch", nullptr, NapiCarbonDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"carbonVersion", nullptr, NapiCarbonVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscarbon_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscarbon_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

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

add_library(carbon_napi SHARED napi_init.cpp)
target_link_libraries(carbon_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoscarbon)

OHOS_ARCH 为空或 libohoscarbon.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const carbonDispatch: (request: string) => string;
export const carbonVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 启动握手和主题加载
aboutToAppear(): void {
  this.version = carbonVersion();          // 版本握手:来自真实 Native 调用
  this.loadMeta();
  this.loadTheme('white');
  this.loadSpacing();
  this.loadTypography();
  this.loadMotion();
}

private loadTheme(name: string): void {
  const res = JSON.parse(this.dispatch('theme', name)) as ThemeResponse;
  if (!res.ok) { this.status = `theme ${name} 失败: ${res.error ?? ''}`; return; }
  this.colors = res.colors; /* …五个分组、深浅色标志… */
  this.status = `${label} 已加载 · ${Object.keys(res.colors).length} 核心 token 来自 Kotlin/Native 注册表`;
}

取色统一走 c(key, fallback) 帮助函数:this.colors['background'] 这类 Record 索引直接作为 ArkUI 的 ResourceColor 字符串使用,非法主题返回 ok:false 进状态行,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行与日志。

5.2 页面交互

五 Tab 自上而下:色板(13 个命名分区 + 其它的过滤 chips,每 token 色块 + 名称 + 十六进制值)、排版(26 个 type set 用 .fontSize(style.fontSize).fontWeight(…).lineHeight(…).letterSpacing(…) 实尺寸渲染,附 名称 · 家族 字号/行高 · 字重 标注)、间距动效(间距条实宽 = token dp、时长表、缓动控制点、按钮高度表)、组件(按钮 6 变体 × 3 尺寸、10 色标签、复选 / 单选 / 开关、双进度条、4 态内联通知)、关于(版本握手行、数据源、token 计数、ArkTS → NAPI → C ABI → Kotlin 注册表 → JSON 分层说明与仓库 / 社区链接)。

主题切换是页面的"验收戏眼":顶部四选一切换后,页面背景、文字、卡片、边框全部取自新主题的 background / textPrimary / layer01 / borderSubtle01……深色主题(g90 / g100)整页反转,证明主题引擎数据真的来自 so。

5.3 对比度和错误边界

组件页配色严格对齐 token 语义:Primary 品牌蓝白字、Danger 红、Tertiary 透明底描边、Disabled 灰底灰字;通知四态用 notificationBackground* 配 support* 圆点;ok:false 时错误原文进状态行。所有交互控件(复选 / 单选 / 开关)的选中色取 interactive token,深浅主题下自动跟随。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoscarbon)+ copyKnSo* 回填
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 2 + MiniJson(继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.carbon、versionName 0.7.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/libohoscarbon_api.h
        ├── src/main/cpp/types/libcarbon_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :carbon-tokens:jvmTest                                              # 8 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

# DevEco Studio 打开 example/harmonyApp(注意是子目录,不是仓库根)
# File → Project Structure → Signing Configs 勾选自动签名后:
# Build → Build Hap(s) / App(s)

签名在 DevEco Studio 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。

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

四、完整代码对照

4.1 整体架构

code-gen/src/main/resources/*.json(上游唯一颜色数据源,一字未动)
    │ tools/generate-registry.ps1(可复现生成)
carbon-tokens(纯 Kotlin 注册表,jvm + ohosArm64 + ohosX64 → KLIB)
    │ CarbonTokenData.themes / typeStyles / spacing / motion
Bridge.kt + MiniJson
    │ CarbonDispatch / CarbonVersion
libcarbon_napi.so
    │ carbonDispatch / carbonVersion
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机目录页(五 Tab + 整页换肤)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_carbon、上游模块移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 单条目钉版
build.gradle.kts(根)版本 0.7.1-ohos.1、全局禁用 Ohos*Test* 任务
gradle/wrapper/*Gradle 降 8.14.1(沿 cmp_qrose wrapper)
carbon-tokens/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,零依赖
carbon-tokens/tools/generate-registry.ps1可复现生成器(读 code-gen JSON + foundation 常量)
carbon-tokens/src/commonMain/.../CarbonTokenData.kt生成的注册表(1433 行,4 主题 × 299 token + 全部刻度)
carbon-tokens/src/commonTest/.../CarbonTokenDataValidationTest.kt零漂移闸门(8 项用例)
example/nativeApp/build.gradle.kts双 ABI sharedLib、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(五 Tab)
README.OpenHarmony_CN.md鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
注册表CarbonTokenData.themes4 套主题 × 299 颜色 token(#AARRGGBB 逐字节来自上游 JSON)
注册表CarbonTokenData.typeStyles26 个 type set(家族 / 字号 / 行高 / 字重 / 字距)
注册表CarbonTokenData.spacing / motionDurations / motionEasings13 档间距、6 档时长、6 条缓动
注册表CarbonTokenData.buttonHeightsDp按钮高度刻度(32/40/48/48/64/80)
NativeCarbonDispatch / CarbonVersionC ABI JSON 导出
N-APIcarbonDispatch(req) / carbonVersion()向 ArkTS 暴露能力
ArkTSloadTheme(name)拉取主题并驱动整页换肤

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(this.dispatch('theme', 'g90')) as ThemeResponse;
// res.ok === true;res.colors['interactive'] === '#FF4589FF'
// 页面背景 .backgroundColor(this.c('background'))、正文 .fontColor(this.c('textPrimary'))
val theme = CarbonTokenData.themes.getValue(name)
buildString {
    append("\"colors\":")
    append(stringMap(theme.colors))   // {"background":"#FF161616",…} 原生嵌入
}

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

五、关键决策说明

决策 1:复用数据而不是复用生成器

上游 :code-gen 已经把 JSON 变成 Kotlin——但生成物 import androidx.compose.ui.graphics.Color。改它的生成器为"双口味"要动上游模块;本 fork 的选择是只读它的输入 JSON,生成器重写为 60 行 PowerShell(确定性输出,签入仓库)。上游模块保持零改动,数据等价性交给逐值测试——上游改主题的那一天,重跑生成器加测试全绿,就是同步完成的证明。

决策 2:工具线选 2.2.21-1.0.0 + Gradle 8.14.1,而不是跟随上游 2.4 / 9.6.1

进 ohos 构建的新模块只用标准库,旧工具线完全够;而 2.2.21 线是社区已批准的钉法(qrose / haze)。代价是 Gradle 从上游 9.6.1 降到 8.14.1——上游构建本来就整体让位,这个代价不存在。与 KStore 的选择(被 coroutines 元数据反推到 2.4.20-0.1.0-07、Gradle 留 9.x)相反,原因很简单:token 注册表零依赖,没有 kotlinx 制品线拽着版本走。

决策 3:根构建替换(cmp_qrose 模式),上游模块全部留盘

上游根构建(dokka / vanniktech / apiValidator / AGP classpath / build-logic 约定插件)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。:carbon 等 Compose 模块、:catalog 演示应用、docs/ 文档站全部留盘不进 settings.gradle.kts——它们的存在方式只有这一种:等社区 CMP 鸿蒙线追上上游 Compose 版本,再原样接回。

决策 4:载荷固定 Map<String, String>,桥不绑定 schema

token 名到十六进制字符串的映射原样过桥:上游增删 token,桥零改动;ArkTS 按 token 名取值直接喂给接受字符串的颜色 API。业务语义(哪个 token 用在哪)留在 ArkTS 页面——那本来就是"设计系统的消费者"该待的位置。

决策 5:jvmTest 交叉验证 + 测试零妥协

数据表适配最大的风险是漂移而不是崩溃。8 项 jvmTest 把注册表与上游 JSON 逐值比对(1196 处颜色值 + 键集合全等 + 全部刻度断言),跑的是上游数据源的原始文件——"零漂移"不是声明,是每一行颜色值上的断言。ohos 侧只承担编译与链接,测试统一 JVM,与社区纪律一致。

六、测试与验证

6.1 测试环境

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

6.2 JVM 交叉验证

./gradlew :carbon-tokens:jvmTest

8 项用例全部通过(tests="8" skipped="0" failures="0" errors="0"),测试代码自带的 90 行迷你 JSON 解析器直接读上游 code-gen/src/main/resources/*.json:

测试类用例钉住什么
theme colors match upstream json verbatim4 主题 × (192+15+40+10+21+21) = 1196 处颜色值逐值比对 + 六组键集合全等
token names are identical across the four themes四套主题 token 名单一致
dark and light themes are flagged correctlywhite/g10 浅色、g90/g100 深色
spacing scale matches upstream SpacingScale13 档 2/4/8/…/160
type styles match upstream CarbonTypography26 个字样全字段
motion durations and easings match upstream Motion6 时长 + 6 缓动控制点
button heights match upstream ButtonSize32/40/48/48/64/80
fork metadata is stable版本号 / 数据源标注

6.3 HAP 验证

:carbon-tokens:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: CarbonDispatch / CarbonVersion / libohoscarbon_symbols 均为全局 T 符号(双 ABI)
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs
CompileArkTS: finished(首轮 Row/flexWrap 报错修复后通过)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

双 ABI 产物:libohoscarbon.so arm64 约 2.7 MB、x86_64 约 2.2 MB(含 Kotlin/Native 运行时与全部 token 数据),加各自 libc++_shared.so(约 1.9 / 1.7 MB)与 CMake 产出的 libcarbon_napi.so。

6.4 功能用例

  1. 启动后"关于"Tab 出现版本握手 0.7.1-ohos.1(来自真实 Native 调用,非静态文字);
  2. 状态行出现「White 已加载 · 192 核心 token 来自 Kotlin/Native 注册表」;
  3. 色板 Tab 逐分区展示色块:背景区第一个色块为 White 主题 background = #FFFFFFFF,交互蓝 interactive = #FF0F62FE 与 Carbon 品牌蓝一致;
  4. 切换 Gray 90 / Gray 100,整页换肤为深色,background 变 #FF161616 / #FF262626,interactive 变 #FF4589FF——主题引擎数据真实来自 so;
  5. 排版 Tab 26 个字样按真实字号 / 行高 / 字重渲染,规格标注与渲染一致;
  6. 间距 Tab 间距条实际宽度与标注 dp 一致(2/4/8/…/160);
  7. 组件 Tab 按钮、10 色标签、4 态通知的取色与色板中的 buttonColors / tagColors / notificationBackground* 分组逐一对得上;
  8. 杀进程重启,再次切换主题数据依旧(注册表静态编译在 so 内,无网络依赖);
  9. hdc shell hilog | grep CarbonNapi 可见每次 dispatch 的字节数日志。

6.5 验证结论

token 注册表、序列化边界、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的设计 token 会话:版本握手、四主题整页换肤、色板逐值渲染与组件复刻(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/carbon-compose.git
cd carbon-compose
export JAVA_HOME="/path/to/jdk-21"
./gradlew :carbon-tokens:compileKotlinOhosArm64 :carbon-tokens:compileKotlinOhosX64   # klib
./gradlew :carbon-tokens:jvmTest                                                    # 8 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp,自动签名后:
#   Build → Build Hap(s) / App(s)
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.carbon -m entry
hdc shell hilog | grep CarbonNapi

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 纯 UI 库的适配面要重新定义:挡在 ohos 前面的不是平台 API,是 Compose 运行时;此时该盘点的是"数据资产在哪",而不是"哪些文件能编"——carbon-compose 的答案是 code-gen 的 4 份 JSON;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 不允许对象成员之间用逗号:生成器最初给每个 public val 成员收尾写了 ,(模仿 mapOf 条目风格),编译器在每个成员处报 “Property getter or setter expected”;成员级收尾必须是裸 );
  4. Int 字面量不会自动转 Double 参数:letterSpacingSp = 0、p1y = 0 必须生成 0.0,生成器为此加了 Format-Double;
  5. 原始字符串里的多层嵌套模板在 KN 编译器上解析失败:"""…${jsonString("… ${x} …")}…""" 两层嵌套连锁炸出 60+ 个错误;先把插值提为局部变量、保持原始字符串内单层模板(qrose 的桥一直是单层写法,不是巧合);
  6. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  7. entry/libs/ 是 gitignore 的,clone 后必须先跑 link 任务回填三件套,否则 DevEco 的 CMake 阶段直接 FATAL_ERROR: Missing libohoscarbon.so;
  8. DevEco 打开的是 example/harmonyApp 子目录,不是仓库根——仓库根是 Gradle 工程,两套构建互不认识;
  9. .flexWrap() 是 Flex 的属性、不存在于 Row:ArkTS 首轮编译在真机构建时才暴露(Property 'flexWrap' does not exist on type 'RowAttribute');需要换行的横向容器直接用 Flex({ wrap: FlexWrap.Wrap }),子项间距用 margin;
  10. ArkTS 严格模式下对象字面量依赖上下文类型:三元表达式里两份字面量可能失去推断,显式声明类型再逐字段赋值最稳;
  11. Windows 上并跑多个仓库构建时 Gradle daemon 偶发 native OOM 崩溃(arena.cpp),停掉闲置 daemon 重试即可,与仓库配置无关;
  12. link 时的 CAdapter clang 警告(api.cpp … non-void function does not return a value in all control paths)来自 HarmonyOS Kotlin 生成的胶水,不是业务代码,so 照常产出(与 qrose 记录一致)。

8.2 已知问题

  • Compose 组件层(285 处 @Composable)不跨桥:社区鸿蒙 CMP 线(1.9.2 / Kotlin 2.2.21)承载不了上游 Compose 1.11.1;组件以 ArkTS 复刻验收,Compose 源码留盘待工具链对齐;
  • 上游 12 个 ImageBitmap 手绘图标不在注册表(依赖 Compose 图形原语),ArkTS 页面未使用图标;
  • CarbonDesignSystem {} / CarbonTheme {} 等 Compose 入口天然不存在于鸿蒙侧,注册表只提供数据;
  • IBM Plex 字体(.otf,Compose resources)不随 so 分发,验收页用系统字体族近似展示字样尺寸,字族度量与 Plex 有细微差异;
  • android / ios / wasm 目标剥离(源码留在树里),发布、dokka、kover、apiValidation 一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.carbon 匹配的签名。

8.3 未来优化方向

  • 生成器进 CI:上游 JSON 变更时自动重生成 + jvmTest 拦截漂移;
  • 把 4 个尺寸枚举的完整 dp 刻度、图标路径数据纳入注册表;
  • 评估 .har 形态发布 token 包,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 等社区 CMP 鸿蒙线追上上游 Compose 版本后,评估 :carbon 组件层直接编译;
  • 补模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 carbon-compose(纯 UI 的 CMP 设计系统库)
    → 盘点数据资产:code-gen 的 4 份 @carbon/themes JSON + foundation 刻度常量
    → 工具链对齐 Kotlin 2.2.21-1.0.0 / Gradle 8.14.1(零依赖,旧线足够)
    → 根构建替换(cmp_qrose 模式,上游模块零改动留盘)
    → carbon-tokens 纯 Kotlin 注册表(4 主题 × 299 token + 26 字样 + 间距 + 动效)
    → 零漂移闸门:jvmTest 与上游 JSON 逐值比对(1196 处颜色值)
    → C ABI(@CName × 2)+ Map<String,String> 载荷
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析
    → ArkUI 真机五 Tab 目录验收(四主题整页换肤 / 色板 / 排版 / 组件)

9.2 封装层次

  • 数据层:上游 @carbon/themes 11.75.0 JSON(唯一颜色数据源,一字未动);
  • 注册表层:CarbonTokenData(纯 Kotlin、零依赖,jvm + 双 ohos KLIB);
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、主题换肤、JSON 解析、组件复刻与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 纯 UI 库的适配先找"数据资产"再找"可编译文件":carbon-compose 能上鸿蒙的不是某个 .kt,而是设计系统本身的数据模型;找到唯一数据源(code-gen JSON),适配面就从 285 个组件缩小到一张表——路线选择必须基于"什么能被逐值证明没漂",而不是"什么文件碰巧不依赖 Compose";
  2. 零依赖是工具链自由的根源:因为注册表连 kotlinx 都不引,版本选择不再被制品线反推,可以沿用社区最成熟的 2.2.21 + Gradle 8.14.1 旧线;依赖越少,鸿蒙工具链的兼容矩阵越小;
  3. 数据型适配的验收闸门是逐值测试而不是能跑:页面显示色块不等于数据正确;1196 处颜色值上的断言 + 上游 JSON 原文件作为测试输入,才让"零漂移"可审计——这与 KStore 用 51 项 desktopTest 钉住引擎语义是同一条纪律在不同库形态上的投影。

9.4 适配成果

当前适配已完成:

  • carbon-compose 0.7.1 设计 token 引擎的双 ABI Kotlin/Native 编译(4 主题 × 299 颜色 token、26 字样、13 间距、6+6 动效、按钮高度刻度,零第三方依赖);
  • 可复现生成器(确定性输出,签入仓库)与上游 :code-gen JSON 数据源的完整落地;
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证双 ABI 全局 T 符号);
  • DevEco Stage ArkUI 五 Tab 验收页面(四主题整页换肤、色板、排版、间距动效、组件复刻、版本握手);
  • 8 项 JVM 交叉验证全绿(1196 处颜色值逐值比对)+ 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐