客户端/服务器架构的 Web 交互式计算环境,复用 OpenHarmony Electron 引擎封成原生应用,仅作 Chromium 窗口壳经 localhost 连本地 Jupyter Server,Python 由设备端解释器或打包 CPython 提供,最终构建出可原生运行的 JupyterLab 应用。
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 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.json5 的 deviceTypes;内置的 .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.mjs 把 pkg/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.ohos 跟 build-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 静态服务,而不是真 Python(jupyterlab.js 的 startLocalServer → startNodeStaticLocalServer,实现在 staticLabServer.js)。这是这个移植里改动最大的一处取舍。在鸿蒙设备上跑完整 Python 版 Jupyter Server,会牵扯到 pyzmq / libsodium 这类原生扩展,它们在 OHOS 上启动时容易崩。所以用 Node/Electron 实现了一个轻量服务:把随包内置的 JupyterLab 前端发出来,再提供一小套兼容 Jupyter Server 的 REST 接口,让前端能加载、能跑起来,不碰那条原生栈。代码里 startLocalServer() 的逻辑是:如果是鸿蒙且没强制要真 Python(JUPYTERLAB_OHOS_SERVER_MODE=python 或 JUPYTERLAB_OHOS_FORCE_PYTHON),就走 startNodeStaticLocalServer(),否则才 startPythonLocalServer()。
保留真 Python 这条路,按需开启(serverConfig.js 的 resolvePythonPath)。如果你确实要在设备上跑真内核,运行时会按这个优先级找 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 的 BrowserWindow,autoHideMenuBar: true,并 Menu.setApplicationMenu(null) 把桌面那套系统菜单栏直接去掉——OHOS 上没有桌面那种顶部菜单栏的范式。webPreferences 里 contextIsolation: 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.INTERNET 和 ohos.permission.GET_NETWORK_INFO——前端和后端走的是 127.0.0.1 上的 HTTP/WebSocket,需要网络权限。原生 .so(libelectron.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 应用。
定制我的领域