把 JupyterLab 搬到鸿蒙 PC:Electron 壳 + 浏览器内 Python 内核的移植实战

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_jupyter
在这里插入图片描述

一、为什么是 JupyterLab,为什么这么难

JupyterLab 是 Jupyter 生态的下一代交互式开发环境——notebook、代码控制台、文件浏览器、多文档工作区(VS Code 式布局)一体化。数据科学、教学、脚本实验场景里它几乎是标配。

但把它搬到鸿蒙 PC 上,难点比一般的 Electron 应用多一层:

  1. 它是 Electron 应用——鸿蒙 PC 没有 Node.js 运行时,也没有 Chromium 壳,需要 OpenHarmony Electron 移植层(libelectron.so
  2. 它依赖一个 Jupyter Server——正常部署是 python -m jupyterlab 起本地服务,前端连 WebSocket。鸿蒙 PC 上跑完整 CPython Jupyter Server 意味着要交叉编译整个 Python 科学栈
  3. 它和 Jupyter Notebook 是「双胞胎交付」——Notebook 7 的前端本来就基于 JupyterLab 组件构建,两个产品 90% 代码同源,但任务清单要求分列交付、独立安装、独立评分

这三条决定了移植策略:Electron 壳承载 + 服务端轻量化 + 产品身份 fork
在这里插入图片描述

题图:JupyterLab 在鸿蒙 PC 真机上运行——notebook 单元格执行、ASCII 艺术字输出、底部状态栏显示 Python 3 (Skulpt) 内核


二、总体架构:三层拆解

在这里插入图片描述

2.1 三种服务模式

模式环境变量说明
node-static(OHOS 默认)JUPYTERLAB_OHOS_SERVER_MODE=node-static主进程用 node-static 托管 Lab 前端静态资源,Python 内核跑在浏览器里
pythonJUPYTERLAB_OHOS_FORCE_PYTHON=1设备上启动 python -m jupyterlab(需要打包 aarch64 CPython)
externalJUPYTERLAB_EXTERNAL_SERVER_URL=http://…连接局域网内已有 Jupyter Server

为什么默认 node-static? 这是整个适配里最重要的一个决策:

  • 打包完整 CPython + Jupyter Server + 依赖树,HAP 体积会失控,而且 numpy/scipy 这类 C 扩展在 OHOS aarch64 上没有现成 wheel,交叉编译是无底洞
  • node-static 模式下,Lab 前端以纯静态资源运行,Python 内核换成浏览器内解释器——真机截图里状态栏显示的 Python 3 (Skulpt) 就是它
  • 「打开 / 编辑 / 保存 notebook + 执行单元格」这个核心闭环全部走通,体积只有纯前端的量级

代价很明确:Skulpt 是纯 JS 实现的 Python 子集解释器,printimport time/os、函数定义、循环、字符串格式化都没问题(下文真机验证),但 numpy/pandas 这类 C 扩展生态不存在。这是「能用的轻量版」和「跑不起来的完整版」之间的取舍——先让核心闭环跑起来。

2.2 和 Jupyter Notebook 工程的关系

任务清单里 JUPYTERJupyter Notebook 是两个独立交付物。技术上 Notebook 7 就是 Lab 内核换壳,所以我从已验证的 ohos_JupyterNotebook 工程 fork 出本仓库,用脚本完成「产品身份 Lab 化」:

node scripts/bootstrap-from-notebook.mjs          # 仅 runtime
node scripts/bootstrap-from-notebook.mjs --with-hap  # 完整 HAP 树

这个脚本做的重写包括:

  • bundle 名:org.jupyter.notebook.ohosorg.jupyter.lab.ohos
  • UI 字符串:Notebook → JupyterLab
  • 环境变量优先级:envFirst('NOTEBOOK_…', 'JUPYTERLAB_…') → Lab 优先
  • HNP 包类型强制 private(原因见第四节坑三)

好处是约 2000 行 Electron 主进程代码(jupyterlab.js / staticLabServer.js)不用维护两份,坏处是所有「身份相关」的东西都要仔细核对——包括签名、HNP、应用名,每一个都变成了坑。


在这里插入图片描述

三、构建链路

pkg/ohos/runtime (Electron 主进程源码)
        │  node pkg/ohos/build-package.mjs
        ▼
ohos_hap/web_engine/src/main/resources/resfile/resources/app
        │  node pkg/ohos/build-hap.mjs  (内部调 hvigor assembleHap)
        ▼
electron-default-signed.hap  →  hdc install

一个极易搞错的细节:Electron 应用的资源树必须放进 web_engine 模块的 resfile:

ohos_hap/web_engine/src/main/resources/resfile/resources/app   ← 正确
ohos_hap/electron/src/main/resources/resfile/...               ← 不会被打包!

entry 模块(electron/)只是壳,真正的 Electron runtime 和应用资源都在 web_engine HAR 里。路径放错的话构建不报错,装上就是白屏。


四、四个真机坑,每一个都值得记录

这部分是整篇文章最有价值的部分。四个坑全部来自真实构建日志,按踩中顺序排列。

坑一:CompileArkTS 10705000(import lazy

ERROR: ArkTS Compiler Error
10705000 ... 'import lazy { ... }'

现象:web_engine 模块大量使用 import lazy 语法,编译直接拒绝。

排查import lazy 需要 API 12 beta3+ 的工具链开关。而 build-profile.json5 里 DevEco 同步时写入了:

"compatibleSdkVersion": "6.1.0(23)",
"compatibleSdkVersionStage": "beta1"

诡异的地方在于:本机装的明明是 Release SDK,但这个 beta1 stage 组合会让 es2abc(ArkTS 编译器)直接拒绝 lazy import 语法。

修复:把 product 钉在已验证可编译的组合上,且不写 stage

"compatibleSdkVersion": "6.0.1(21)",
"targetSdkVersion": "6.0.1(21)"
// 不要 compatibleSdkVersionStage

后患:DevEco 的 Project Structure / Sync / 签名 Fix 操作都可能把这一块悄悄改回去。每次在 DevEco 里动过配置,重新构建前都要 grep 一遍这个字段。

坑二:SignHap 00303074(profile 绑定 bundle)

SignHap failed: 00303074

现象:签名阶段失败。

根因:鸿蒙的自动签名 debug profile 是绑定 bundleName 的。fork 工程时 build-profile.json5 里残留的签名材料是 Notebook 的 .p7b——它只包含 org.jupyter.notebook.ohos,拿去签 org.jupyter.lab.ohos 直接被拒。

修复

  1. 清空工程里的 signingConfigs(bootstrap 脚本会自动做)
  2. DevEco → Project Structure → Signing Configs → 自动签名 / Fix,为 Lab 自己的 bundle 重新生成 .p7b

教训:fork 任何鸿蒙工程,签名材料都要视为「每 bundle 一份」,绝不复用。

坑三:Install 9568407(HNP 原生包冲突)

Install Failed: code:9568407
Failed to install the HAP because installing the native package failed.

hilog 里的关键线索:

ProcessBundleInstallNative … hnp install: electron
[HNP API] native package install! … package name=org.jupyter.lab.ohos
already exist cfg ignore
… MSG_ERR_NATIVE_INSTALL_FAILED

现象:Notebook 已安装的情况下,Lab 装不上;卸了 Notebook,Lab 就能装。

根因:两个产品都声明了同名 HNP 原生包 jupyterlab_python.hnp(打包 Python 运行时用的)。Notebook 声明的类型是 "public"(设备全局唯一),Lab 再声明同名 public 包,系统检测到「已存在同名配置」直接失败。

修复:Lab 侧把 HNP 声明改为应用私有:

// ohos_hap/electron/src/main/module.json5
"hnpPackages": [
  { "package": "jupyterlab_python.hnp", "type": "private" }
]

private 类型的 HNP 作用域是单个应用,Lab 和 Notebook 各自持有一份,互不干扰,两个产品从此可以共存。这个改动要重新 assembleHap,因为模块元数据是烘焙进 HAP 的。

坑四:Install 9568320(no signature file)

Install Failed: error: failed to install bundle.
code:9568320
error: no signature file.

现象:明明刚在 DevEco 里做完了自动签名(~/.ohos/config/ 里的材料时间戳就是刚才),构建也成功了,装机还是说没有签名。

排查:看产物文件名——electron-default-unsigned.hapunsigned 三个字就在文件名里

根因:DevEco 的自动签名只负责两件事——生成材料、写入 build-profile.json5signingConfigs 块。但 products[].signingConfig 这个引用字段它不管:

"signingConfigs": [
  { "name": "default", "material": { ... } }   // ← 材料齐全
],
"products": [
  { "name": "default",
    "signingConfig": "",                        // ← 留空!签名任务被跳过

signingConfig: "" 时 hvigor 会静默跳过 SignHap,产出 unsigned HAP,构建全程无警告,直到 bm install 才报 9568320。

修复

"signingConfig": "default",   // 指向 signingConfigs 里的配置名

重新构建后 SignHap 任务执行 12 秒,产物变成 electron-default-signed.hap,装机一次通过。

这个坑我在同一天踩了三次(Terminator、JupyterLab、JupyterNotebook 三个工程),可以确认它是 DevEco 的系统性问题:自动签名永远不回填 products 引用。判断方法就一条:构建完看产物文件名,带 unsigned 就是没签上。


五、真机验收:七个功能点逐一过

设备:HUAWEI MateBook Pro,HarmonyOS 7.0.0。安装产物 electron-default-signed.hap 约 528MB(含 Electron runtime + Lab 前端 + HNP Python 包)。

5.1 Launcher 启动器

启动后首先进入 Launcher——这是 JupyterLab 区别于 Notebook 的标志性界面,新建入口集中在一屏:

Launcher:新建 Notebook(Python 3 (Skulpt))、Text File、Markdown File、Python File 全部可用

左侧文件浏览器、右侧 Launcher 卡片布局完整渲染,GPU 合成关闭的情况下滚动无明显掉帧。

5.2 Notebook 编辑与执行

核心闭环。新建 Untitled.ipynb,逐格输入并执行:

notebook 执行中:[1] print(1) 输出 1,[2] 执行完成,[3] 正在编辑,Command 模式状态栏实时反映

In/Out 标记、单元格编号自增、模式指示(Command/Edit)、底部状态栏的 Cell n/n 与行列号都在工作——这些细节全部来自 Lab 前端本身,说明静态化部署没有破坏其状态管理。

再跑一段更复杂的:import time/os、函数定义、循环、以及一段生成 ASCII 大字的字符串代码:

notebook 连续执行三个单元格:基础 print、模块导入 + ASCII 艺术字输出、clear/main/动态加载动画函数,底部状态栏 Python 3 (Skupert) | Idle

import timeimport os 成功、函数定义与调用成功、多行字符串拼接输出成功——Skulpt 内核的执行链路完整。

5.3 Python 文件编辑器

除了 notebook,Lab 的多文档能力也要验证。通过 Launcher 新建 Python File,写入同样的代码:

案例.py:Python 文件编辑器视图,代码高亮、行号、底部 Python 状态指示正常

语法高亮、行号、编辑器/控制台双模式都正常。

5.4 菜单完整度

Electron 壳最容易出问题的就是原生菜单。实测 File 菜单完整展开:

File 菜单:New / New Launcher / Open from Path / Save As / Reload / Rename / Print 等完整菜单项,快捷键提示齐全

从 New 到 Print 共 20+ 个菜单项,快捷键标注(Ctrl+Shift+L、Alt+W、Ctrl+Shift+Q…)一项不少——这些是 Lab 前端自绘的菜单,不是 OS 原生菜单,所以行为在 Electron 壳里是自洽的。

5.5 文件操作对话框

Rename file 对话框:File → Rename Python File,输入框、确认/取消按钮完整

重命名对话框、输入框焦点、按钮响应全部正常。

5.6 About 弹窗

Help → About JupyterLab:版本信息、贡献者列表、项目主页链接

Help → About JupyterLab 弹窗正常显示——一个容易被忽略但很说明问题的细节:跑在鸿蒙 PC 上的确实是 JupyterLab 本尊,不是仿制的 UI。


六、已知限制(如实记录)

限制说明
内核是 Skulpt,不是完整 CPythonnumpy/pandas/matplotlib 等 C 扩展生态不可用;纯 Python 语法子集没问题
GPU 合成默认关闭OHOS Electron 的硬件加速路径不稳定,实测关掉更稳
大 HAP 体积528MB(Electron runtime + Lab 前端 + HNP),主要是 Electron 基础设施
三个工程踩了同一个签名坑DevEco 自动签名不回填 products[].signingConfig,每次 Fix 后要手动检查
external 模式未在弱网验证JUPYTERLAB_EXTERNAL_SERVER_URL 连远程 Jupyter Server 的路径已实现,等待真机场景

七、复现路径

cd ohos_JupyterLab

# 1. 从 Notebook 工程同步 runtime(首次必做)
node scripts/bootstrap-from-notebook.mjs --with-hap

# 2. 打包 runtime 到 web_engine resfile
node pkg/ohos/build-package.mjs

# 3. 构建 HAP(需 DevEco / hvigor)
node pkg/ohos/build-hap.mjs
# 或直接:
cd ohos_hap && hvigorw assembleHap --mode module -p electron@default -p buildMode=debug

# 4. DevEco 自动签名后,检查并修正 products[].signingConfig = "default"

# 5. 安装
hdc install -r electron/build/default/outputs/default/electron-default-signed.hap
hdc shell aa start -a EntryAbility -b org.jupyter.lab.ohos

环境变量速查(JUPYTERLAB_* 优先,NOTEBOOK_* 兼容):JUPYTERLAB_OPENHARMONY=1JUPYTERLAB_DISABLE_GPU=1JUPYTERLAB_PORT / JUPYTERLAB_TOKEN / JUPYTERLAB_ROOT_DIRJUPYTERLAB_OHOS_SERVER_MODE


常见问题 FAQ

Q1:安装报 9568320 no signature file,但我明明签名了?

看产物文件名。如果是 electron-default-unsigned.hap,说明 build-profile.json5products[].signingConfig 还是空串 ""——DevEco 自动签名只写入 signingConfigs 材料,不会回填这个引用。手动改成 "default" 重新构建即可。

Q2:装上之后白屏,什么都不显示?

九成是资源树放错了模块。Electron 应用资源必须放在 web_engine/src/main/resources/resfile/resources/app,放进 electron/(entry)模块的 resfile 不会被打包,构建也不报错。

Q3:能装 numpy / pandas 吗?

不能。默认内核是 Skulpt(浏览器内纯 JS 实现的 Python 子集),printimport time/os、函数定义这些纯语法没问题,但没有 CPython 的 C 扩展生态。需要科学栈时用 external 模式连远程 Jupyter Server(JUPYTERLAB_EXTERNAL_SERVER_URL)。

Q4:和 Jupyter Notebook 版冲突吗?能不能同时装?

能共存,前提是 Lab 的 HNP 包声明为 "type": "private"。如果两个都装时报 9568407,说明用了同名 public HNP(jupyterlab_python.hnp),把 Lab 侧改成 private 重新打包即可。

Q5:编译报 10705000import lazy 不认识?

build-profile.json5 里出现了 compatibleSdkVersionStage: "beta1"。删掉这个字段,把 SDK 钉在 compatibleSdkVersion: "6.0.1(21)"。注意 DevEco 每次 Sync / 签名 Fix 都可能把它改回来,动过配置就要检查一遍。

Q6:界面有点卡,能开硬件加速吗?

不建议。OHOS Electron 的 GPU 合成路径目前不稳定,默认已通过 JUPYTERLAB_DISABLE_GPU=1 关闭。真机关 GPU 滚动无明显掉帧,属于可用状态;开 GPU 反而可能出现渲染异常。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐