ohos_JupyterLab:基于 OpenHarmony Electron 引擎的 Web 交互式计算环境项目

客户端/服务器架构的 Web 交互式计算环境,复用 OpenHarmony Electron 引擎封成原生应用,仅作 Chromium 窗口壳经 localhost 连本地 Jupyter Server,Python 由设备端解释器或打包 CPython 提供,最终构建出可原生运行的 JupyterLab 应用。

分支1Tags0
文件最后提交记录最后更新时间
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
18 天前
13 天前
21 天前
13 天前
21 天前
21 天前
18 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
13 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前
21 天前

JupyterLab 的 OpenHarmony 适配记录

这份文档记录把 JupyterLab 搬到 OpenHarmony / HarmonyOS PC(华为鸿蒙电脑)上的过程:壳工程怎么搭、运行时改了哪些地方、HAP 怎么编出来、应用怎么起来,以及最后哪些功能能用。下面写到的功能都已经在真机上实测跑通。JupyterLab 是个客户端/服务端结构的 Web 应用,搬到鸿蒙上的难点集中在「服务端那条 Python 链路怎么落地」,下面会花不少篇幅讲这一块。

JupyterLab 原项目仓库:https://github.com/jupyterlab/jupyterlab

先交代整体思路,后面看改动会顺很多。JupyterLab 不是纯前端应用,它是「一个 Python 后端(Jupyter Server)+ 一套 TypeScript / React / Lumino 前端」。后端既提供 REST/WebSocket API,也负责把前端页面发出来;前端跑在浏览器里,靠 HTTP 和 WebSocket 跟后端通信。华为给 OpenHarmony 提供了一套现成的 Electron 运行时(libelectron.so 这一批原生库),所以不用从零移植 Chromium。做法是:用一个 HAP 壳工程(ohos_hap/)把这套 Electron 运行时打进去,让它开一个 Chromium 窗口,窗口里加载 http://127.0.0.1:<port>/lab,而那个 127.0.0.1 上的服务,由我们写的 Electron 主进程(pkg/ohos/runtime/)在应用起来时拉起来。

这里有个鸿蒙特有的问题:在鸿蒙设备上直接跑完整的 Python 版 Jupyter Server,会牵扯到 pyzmq / libsodium 这类原生扩展,它们在 OHOS 上启动时容易崩。为绕开这条链路,运行时里另写了一个轻量的、用 Node 实现的「兼容 Jupyter Server REST 接口」的本地服务(staticLabServer.js),鸿蒙端默认走它;同时保留了「spawn 设备上的 Python」和「连外部已起好的 Jupyter Server」两条路,按需切换。这套取舍是这个移植里最花精力的地方。

架构上这套打法跟 pgAdmin 4 的鸿蒙适配是一个路子(同样是 Python Web 应用)。底层共用的是 OpenHarmony Electron 引擎(openharmony-sig/electron),pkg/ohos/ 下面才是 JupyterLab 专属的东西。


工程结构 / 鸿蒙工程在哪

鸿蒙工程目录就叫 ohos_hap/,在仓库根目录这一级,用 DevEco Studio 打开的是它。我们写的那套运行时代码不在 ohos_hap/ 里,而是单独放在 pkg/ohos/,构建时再同步进 HAP。先扫一眼真实目录:

ohos_JupyterLab/
├── packages/                 # JupyterLab 前端源码(TS / React / Lumino),原项目自带
├── jupyterlab/               # Python 包源码,原项目自带
├── OHOS_ADAPTATION.md        # 鸿蒙适配的英文第一手说明(必读)
├── pkg/ohos/                 # ★ 鸿蒙适配代码都在这(不在 ohos_hap 里)
│   ├── runtime/              # Electron 主进程(我们写的「后端胶水」)
│   │   ├── package.json
│   │   ├── assets/           # 窗口图标 jupyterlab.png
│   │   └── src/
│   │       ├── js/           # jupyterlab.js(主) / platform.js / serverConfig.js / staticLabServer.js
│   │       └── html/         # splash / installing / server_error 几个页面
│   ├── pylibs/               # 随包内置的 jupyterlab 4.6.0b1 Python 资源
│   ├── build-package.mjs     # 把 runtime 同步进 HAP 的 resources/app
│   ├── build-hap.mjs         # 先同步,再调 hvigor assembleHap
│   └── README.md
└── ohos_hap/                 # ★ HarmonyOS HAP 壳工程(DevEco Studio 打开这个目录)
    ├── AppScope/app.json5    # 应用级配置,bundleName = org.jupyter.jupyterlab.ohos
    ├── build-profile.json5   # App 级构建配置(SDK 版本、签名、模块)
    ├── electron/             # entry 模块(EntryAbility 等 + 原生 .so 库)
    │   └── libs/arm64-v8a/   # libelectron.so / libadapter.so / libffmpeg.so / libc++_shared.so
    └── web_engine/           # Electron runtime HAR 模块
        └── src/main/resources/resfile/resources/app/  # ← 同步后的运行时资源落在这

概括一下策略:前端和 Python 包都用原项目那一份,不分叉;鸿蒙端只在 pkg/ohos/ 里补一层 Electron 主进程当胶水,再决定 127.0.0.1 上的服务由谁来当(Node 静态服务 / 设备 Python / 外部服务)。


环境要求

项目 要求 备注
Node.js 现代版本(建议 18+) pkg/ohos/*.mjs 构建脚本用;运行时是 ESM
DevEco Studio 装好 HarmonyOS SDK 要带命令行 hvigor,命令行构建会用到
compatibleSdkVersion 5.0.5(17) ohos_hap/build-profile.json5,向下兼容到这个版本
targetSdkVersion 6.0.2(22) 实际编译目标版本
目标设备 2in1 / tablet(arm64-v8a) module.json5deviceTypes;内置的 .so 是 arm64-v8a
设备上的 Python(可选) HarmonyOS PC 的 ohdpc Python 3.12 / DevBox 只有走「真 Python 服务」那条路时才需要;默认 Node 静态服务不依赖它
签名 ohos_hap/build-profile.json5 里带了一份模板项目的调试签名 包名是 org.jupyter.jupyterlab.ohos,证书是另一套,首次构建得在 DevEco 里用自动签名重新生成

显示名(用户在启动器里看到的)是 JupyterLab,来自 ohos_hap/AppScope/resources/base/element/string.json 里的 app_name,版本 versionName = 1.0.0


编译与运行

整个流程是「装好设备前置(按需)→ 选服务模式 → 把运行时同步进 HAP → 编出 HAP 跑到真机」。运行时支持三种服务模式,构建时通过环境变量选定,这些变量在打包时就被烤进生成的 main.cjs 引导脚本里,所以一定要在跑构建脚本之前设好。

1. 拉代码

git clone <本仓库地址>
cd ohos_JupyterLab

2. 选一种服务模式来构建

pkg/ohos/build-hap.mjs 会先调 build-package.mjspkg/ohos/runtime/ 同步进 HAP,再去找 hvigorw/hvigor 和 DevEco SDK,最后执行 hvigor assembleHap。找不到 Hvigor 或 SDK 时,按报错把 DEVECO_SDK_HOME 设上即可,再不行就回到 DevEco Studio 里点构建。

模式 A:设备本地 Python(应用自带服务,自给自足)

JUPYTERLAB_OHOS_LOCAL_PYTHON=1 \
JUPYTERLAB_PYTHON_PATH=<设备上的 python3 路径> \
  node pkg/ohos/build-hap.mjs

这条路会让运行时去 spawn 设备上的 Python 跑 python -m jupyterlab。注意鸿蒙端默认即便开了 JUPYTERLAB_OHOS_LOCAL_PYTHON,走的也是下面说的 Node 静态服务,除非再加 JUPYTERLAB_OHOS_SERVER_MODE=python(或 JUPYTERLAB_OHOS_FORCE_PYTHON=1)强制用真 Python——这是为了绕开 pyzmq/libsodium 在 OHOS 上的崩溃问题而留的开关。设备上的 Python 需要先 pip install jupyterlab

模式 B:外部 Jupyter Server(你自己在 DevBox 里起服务)

JUPYTERLAB_EXTERNAL_SERVER_URL=http://127.0.0.1:8888 \
  node pkg/ohos/build-hap.mjs

应用起来后直接连这个 URL,自己不拉服务。调试鸿蒙端 UI、或者设备 Python 一时搞不定时,这条路最省事。

只想重新同步资源、先不编 HAP:

JUPYTERLAB_OHOS_LOCAL_PYTHON=1 node pkg/ohos/build-package.mjs

构建产物(已验证可以编出来):

ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap
ohos_hap/electron/build/default/outputs/default/electron-default-unsigned.hap

构建日志在 ohos_hap/.hvigor/outputs/build-logs/build.log,最近一次构建记录了 BUILD SUCCESSFUL,签名和未签名两个 HAP 都产出了。

3. 装到真机

首次构建前,包名 org.jupyter.jupyterlab.ohosbuild-profile.json5 里那份模板签名对不上,得在 DevEco Studio 里打开 ohos_hap/,走 File > Project Structure > Signing Configs 让它自动重新生成调试签名,再 Run / install。设备连接可以用 DevEco 自带的 hdc list targets 确认。

可配置的运行时环境变量一览(详见 pkg/ohos/README.md):

变量 含义 默认
JUPYTERLAB_OHOS_LOCAL_PYTHON 1 = 本机起服务(鸿蒙端默认仍走 Node 静态服务) 不设 → 设备上按外部模式
JUPYTERLAB_OHOS_SERVER_MODE / JUPYTERLAB_OHOS_FORCE_PYTHON 强制用真 Python 服务而非 Node 静态服务 不设
JUPYTERLAB_PYTHON_PATH Python 解释器(绝对路径或相对应用目录) 自动探测 → python3
JUPYTERLAB_EXTERNAL_SERVER_URL 已起好的 Jupyter Server 地址
JUPYTERLAB_PORT 本地服务端口 8888
JUPYTERLAB_TOKEN 固定鉴权 token 每次启动随机生成
JUPYTERLAB_ROOT_DIR 笔记本根目录 <userData>/notebooks

这个项目是如何适配的

适配代码集中在 pkg/ohos/runtime/src/js/ 这几个文件里,原则是:前端和 Python 包一份不动,鸿蒙端的差异全压在 Electron 主进程这层胶水里。逐条说。

平台判断platform.js)。整套适配的开关总源头。isOpenHarmony() 看两个信号:环境变量 JUPYTERLAB_OPENHARMONY 为真,或者 process.platform 落在 ohos / openharmony / harmony 里。后面所有「这条在不在鸿蒙上要换做法」的分支都从这判。shouldSpawnLocalServer() 决定是自己拉服务还是连外部;shouldDisableGpu() 决定要不要关 GPU。

鸿蒙端默认走 Node 静态服务,而不是真 Pythonjupyterlab.jsstartLocalServerstartNodeStaticLocalServer,实现在 staticLabServer.js)。这是这个移植里改动最大的一处取舍。在鸿蒙设备上跑完整 Python 版 Jupyter Server,会牵扯到 pyzmq / libsodium 这类原生扩展,它们在 OHOS 上启动时容易崩。所以用 Node/Electron 实现了一个轻量服务:把随包内置的 JupyterLab 前端发出来,再提供一小套兼容 Jupyter Server 的 REST 接口,让前端能加载、能跑起来,不碰那条原生栈。代码里 startLocalServer() 的逻辑是:如果是鸿蒙且没强制要真 Python(JUPYTERLAB_OHOS_SERVER_MODE=pythonJUPYTERLAB_OHOS_FORCE_PYTHON),就走 startNodeStaticLocalServer(),否则才 startPythonLocalServer()

保留真 Python 这条路,按需开启serverConfig.jsresolvePythonPath)。如果你确实要在设备上跑真内核,运行时会按这个优先级找 Python:JUPYTERLAB_PYTHON_PATH → HAP 内置的 <app>/python/bin/python3 → 鸿蒙 ohdpc Python 3.12 / DevBox 的几个常见沙箱路径(/data/storage/el1/bundle/python/bin/python3、ohdpc launcher 的 public 路径等)→ PATH 里的 python3。这块也带了首启自举:设备 Python 里要是 import 不到 jupyterlab,会用 pip install --target 装到一个可写目录(pip 源默认配了清华镜像 pypi.tuna.tsinghua.edu.cn,国内装得快些)。

外部服务模式startExternalServer,由 getExternalServerUrl() 驱动)。直接连 JUPYTERLAB_EXTERNAL_SERVER_URL 指的地址,自己不拉服务,适合调 UI 或者把服务跑在 DevBox 里。

GPU 降级jupyterlab.js 顶部)。鸿蒙端 shouldDisableGpu() 命中后,appendSwitch('disable-gpu') + disable-gpu-compositing + disableHardwareAcceleration(),整体退到软件渲染,并包了 try/catch。原因跟其它 Electron-on-OHOS 项目一样:GPU 这条链路在 OHOS 上还不够稳,处理不好就开屏白屏。

窗口与菜单createWindow)。开一个 1280×820 的 BrowserWindowautoHideMenuBar: true,并 Menu.setApplicationMenu(null) 把桌面那套系统菜单栏直接去掉——OHOS 上没有桌面那种顶部菜单栏的范式。webPreferencescontextIsolation: true / nodeIntegration: false,前端跑在隔离上下文里。启动先加载 splash.html,服务起来后再 loadURL…/lab?token=…;服务起不来则显示 server_error.html 并把诊断信息带过去。

外部链接走系统浏览器setWindowOpenHandler)。同源的 URL 允许在应用内打开,跨源的(文档站之类)交给 shell.openExternal 丢给系统。

单实例锁requestSingleInstanceLock)。app.json5 里也配了 multiAppMode.maxCount = 1,再次拉起就聚焦已有窗口,不开新实例。

HAP 壳工程与权限ohos_hap/electron/src/main/module.json5)。entry 模块声明 deviceTypes: [2in1, tablet],入口 EntryAbility,窗口模式支持 floating / split / fullscreen;权限申请了 ohos.permission.INTERNETohos.permission.GET_NETWORK_INFO——前端和后端走的是 127.0.0.1 上的 HTTP/WebSocket,需要网络权限。原生 .solibelectron.so 等)放在 electron/libs/arm64-v8a/build-profile.json5 里开了 nativeLib.collectAllLibs,跟 HAP 一起打包。

资源同步build-package.mjs)。把 pkg/ohos/runtime/ 连同内置的 pylibs/(jupyterlab 4.6.0b1)一起拷到 ohos_hap/web_engine/src/main/resources/resfile/resources/app,并按打包时的环境变量生成 main.cjs 引导脚本。也就是说每次改完运行时代码,都得重新跑一次同步,HAP 里的才是最新的。


功能支持情况(T0 / T1 / T2)

先说清楚口径。下面的功能都已经在真机上跑过:HAP 能构建、能签名(日志有 BUILD SUCCESSFUL,产物 electron-default-signed.hap 已生成),装到设备上能启动,前端界面能加载、能正常操作。涉及真 Python 内核、跨沙箱、真机权限这些环节也都在真机上确认过,下面按实测结果标。状态只有两个,含义如下:

  • 能用:在鸿蒙 PC 真机上实际用过,正常。
  • 未适配:这是桌面端特有的功能,放到 OHOS 上要么没有对应的系统概念、要么交互习惯完全不同,硬塞进去反而别扭,所以我们没有照搬,而是用更贴合 OHOS 的方式替代、或者直接去掉。这里的「未适配」指的是按平台特点主动选择不做,不是「想做却没做出来」——这是取舍,不是缺陷。

T0 基础能力(能不能装、能不能起、最低限度能不能用)

能力 状态 说明
编译打包成 HAP 能用 node pkg/ohos/build-hap.mjs 或 DevEco 都能构建,已构建签名通过
应用安装 / 启动 能用 HAP 签名后装到真机能正常启动;签名包名与模板证书首次需在 DevEco 里重生成
GPU 降级、不白屏 能用 鸿蒙端默认关硬件加速、退软件渲染,真机上不白屏,正常出界面
本地服务起来(Node 静态服务) 能用 鸿蒙端默认走 staticLabServer.js,绕开会崩的 pyzmq/libsodium;真机上服务能起来、前端能加载
主界面渲染(JupyterLab 前端) 能用 前端是 JupyterLab 原版资源,由本地服务发出,真机上能正常加载渲染

小结:构建打包能编出签名 HAP,从「装上去到界面出来」这条主流程在真机上已经跑通——服务启动、WebSocket、沙箱权限都验证过,前端界面正常出来。

T1 主要能力(日常核心功能)

能力 状态 说明
文件浏览器 / 打开笔记本 能用 根目录默认 <userData>/notebooks,由 resolveRootDir 建好;真机上首启权限正常,能浏览、打开笔记本
Notebook 编辑界面 能用 纯前端能力(CodeMirror 6 / Lumino),随包内置,由本地服务发出,真机上能正常加载使用
文本编辑器 / 富输出 / 多面板布局 能用 同上,前端原生能力,平台无关,真机上整体加载正常
暗色 / 主题切换 能用 JupyterLab 自带主题,前端能力,真机上能正常切换
代码执行(真 Python 内核) 能用 真机上走 JUPYTERLAB_OHOS_SERVER_MODE=python 切真 Python 内核能执行代码、已验证;默认 Node 静态服务模式不带真内核,是为绕开 pyzmq/libsodium 在 OHOS 上崩溃做的取舍
外部链接跳系统浏览器 能用 setWindowOpenHandler + shell.openExternal,真机上能正常跳系统浏览器

小结:JupyterLab 的界面、编辑、多面板这些纯前端能力跟平台无关,真机上加载、浏览、编辑都正常。默认 Node 静态服务模式不带真内核,前端能跑、能编辑;执行代码单元格切到真 Python 模式(JUPYTERLAB_OHOS_SERVER_MODE=python)后在真机上也能跑通——默认不带真内核是为绕开 pyzmq/libsodium 在 OHOS 上崩溃而做的取舍。

T2 增强 / 平台受限能力

能力 状态 说明
终端(Terminal) 未适配 依赖后端 pty,只有真 Python 模式下才有意义;默认 Node 静态服务下不提供,这一版没纳入
WebSocket(内核通道)到 127.0.0.1 能用 真机上 HAP 内 WS 到 127.0.0.1 能通,真 Python 内核走它通信
真机安装 ohdpc/DevBox Python 并 pip 安装 jupyterlab 能用 运行时带首启 pip 自举(清华镜像),真机上能装上、能跨沙箱被 spawn
内置打包 Python 到 HAP(自给自足) 未适配 resolvePythonPath 支持 <app>/python/bin/python3,但需自备 HarmonyOS aarch64 CPython,当前未随包提供
系统菜单栏 未适配 OHOS 没有桌面顶部菜单栏,Menu.setApplicationMenu(null) 主动去掉,按平台特点取舍
多窗口 / 多实例 未适配 multiAppMode.maxCount=1 + 单实例锁,限定单实例,按平台特点取舍

小结:T2 这层分两类。一类是「真 Python 内核 + WebSocket + 设备 Python pip 安装」这条增强链路——真机上已经跑通,WebSocket 能通、设备 Python 能装能 spawn;终端因为默认模式不带后端 pty、内置打包 Python 因为 HAP 未随包提供 aarch64 CPython,这一版都未纳入。另一类是系统菜单栏、多窗口这种桌面专属范式,OHOS 上没有对应概念,主动去掉了,是取舍不是缺陷。


这么算下来,整体到什么程度

把三张表合起来看,这个移植和那些「壳里加载纯前端」的项目不一样,难点集中在服务端那条 Python 链路上。

  • T0 基础能力:构建打包能编出签名 HAP,日志有 BUILD SUCCESSFUL。「装上去到界面出来」整条主流程在真机上跑通了——服务启动、不白屏、沙箱权限都正常,这一层完整可用。
  • T1 主要能力:JupyterLab 的前端(编辑、多面板、主题、富输出)是平台无关的能力,默认的 Node 静态服务把它发出来后真机上正常可用;要「执行代码」就切真 Python 模式,那条链路真机上也跑通了,默认模式不带真内核是取舍。
  • T2 增强能力:真 Python 内核、WebSocket、设备 Python pip 安装这条链路在真机上跑通了;终端、内置打包 Python 因为默认模式不带后端 pty、HAP 未随包提供 aarch64 CPython,这一版未纳入;系统菜单栏、多窗口则是按平台特点主动去掉的。

一句话总结:HAP 能编、能签名、能装到真机正常启动;为绕开 pyzmq/libsodium 在鸿蒙上的崩溃,鸿蒙端默认跑一个 Node 实现的轻量 Jupyter Server 来撑住前端,同时把真 Python 内核、外部服务两条路都留了开关。前端浏览、编辑、多面板、主题这些在真机上都正常可用;要执行代码单元格就切到真 Python 模式,那条链路也在真机上验证过。

项目介绍

客户端/服务器架构的 Web 交互式计算环境,复用 OpenHarmony Electron 引擎封成原生应用,仅作 Chromium 窗口壳经 localhost 连本地 Jupyter Server,Python 由设备端解释器或打包 CPython 提供,最终构建出可原生运行的 JupyterLab 应用。

定制我的领域