A
Air官方
【Air 系列 · 鸿蒙 PC 适配全记录】44 / 56
■ 应用:AirShelf(from Calibre)(AirShelf)
■ 定位:Calibre 移植,电子书库管理
■ 上游:calibre 9.13.0 | 路线:Qt6+Python 运行时 | 版本:1.1.0
■ 开源: https://gitcode.com/Andy88/Harmony-calibre
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917615794843877527
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-calibre
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirShelf
calibre 是开源世界中最完整的电子书库管理软件:书库编目、元数据编辑、格式转换、设备传输、内置阅读器与编辑器,全部围绕一个本地书库组织。对经常与 epub、mobi、azw3、txt 打交道的读者来说,它长期是桌面端不可绕开的基础设施。AirShelf 把 calibre 9.13.0 完整带到鸿蒙侧,选择的是一条此前 Air 系列从未走过的路:不重写、不仿制,直接把这套 1456 个 Python 模块与数十个 C 扩展组成的桌面栈,连同一个完整的 CPython 运行时,交叉编译进 arm64-v8a 的鸿蒙应用沙箱。
这个工程的独特价值在于检验了"动态语言大型桌面应用"的移植路径。Qt Widgets 类 C++ 项目在 Air 系列已有成熟范式,而 calibre 的难点叠加了一层:PyQt6 的 sip 绑定要针对目标平台从源码构建,Python 解释器要以 libpython3.14.so 形态嵌入应用进程,上千个纯 Python 模块还要解决随包分发与运行时定位。这些环节一旦打通,后续任何 Python 系桌面软件的鸿蒙移植都有据可依。
工程概况:包名 com.airshelf.app(AppScope/app.json5),版本 1.0.0,deviceTypes 覆盖 phone、tablet、2in1;目标 HarmonyOS 6.1(API 23)以上,Native ABI 为 arm64-v8a;内核采用 Air 系 Qt6 范式(Qt 6.11.1 预编译栈 + ohos QPA + 薄 NAPI 桥 + 重型 core),叠加 Air 系首个 Python 运行时。PORTING.md 以 PATCH-AIRSHELF-N 编号登记全部树内补丁与踩坑对策,是本文事实的主要来源。
▍二、先划清边界:不把编译通过当成适配完成
calibre 的桌面形态建立在一组鸿蒙侧不存在的底座上:X11/Wayland 窗口系统、QtWebEngine(Chromium)、独立进程的阅读器与编辑器、USB/MTP 设备层、以及默认可用的联网生态。把这些全部照搬既不可行也不必要,工程的处理方式是逐项裁决:能进程内重建的重建(阅读器、编辑器),必须依赖系统沙箱能力替代的替代(文件选择器、IME、书库目录),与移动形态冲突或有害的明确移除(捐赠、联网商店、新闻抓取、内容服务器)。
层次 原项目实现(传统桌面方案) 鸿蒙侧当前方案
启动入口 calibre 启动器拉起 Python 进程 EntryAbility + Index.ets XComponent,NAPI 桥启动 core
GUI 栈 PyQt6 + X11/Wayland 窗口系统 PyQt6 + 自研 ohos QPA(libqohos.so,单 EGLSurface)
Python 运行时 系统或官方 CPython 安装 CPython 3.14.7 交叉编译,libpython3.14.so 随 HAP 分发
内置阅读器 QtWebEngine 独立 viewer 进程 进程内 gui2/airshelf_reader.py(QTextBrowser 实现)
书籍编辑器 ebook-edit 独立进程 进程内 gui2/airshelf_editor.py 顶层窗口
菜单/对话框 原生 QMenuBar / QFileDialog QMenuBar JSON 快照 + ArkTS bindMenu;qohosfiledialoghelper + DocumentViewPicker
设备传输 USB/MTP(udisks) 整层禁用(PATCH-AIRSHELF-3)
联网能力 商店/新闻/内容服务器/捐赠 全量移除(PATCH-AIRSHELF-15 与后续各轮)
书库存储 ~/Calibre Library Download/com.airshelf.app/ 授权目录,跨卸载存活(PATCH-AIRSHELF-34)
边界的意义在于每个降级都有明确编号与验收记录:QtWebEngine 不可交叉(Chromium)对应 PATCH-AIRSHELF-1 的优雅降级守卫;viewer 源码不随包分发,双击书行的默认动作改为编辑元数据,阅读入口接管为自研阅读器。当前产物是可安装、可建库、可转换、可阅读的完整应用,而不是 calibre 界面的静态演示。
▍三、鸿蒙版的整体架构
运行时自顶向下分四层。ArkTS 壳负责 Ability 生命周期、XComponent SURFACE、系统选择器与 IME 桥、菜单栏快照轮询以及 rawfile 数据幂等部署;薄 NAPI 桥只链系统库,按拓扑序 dlopen 依赖栈后进入核心;核心线程内完成 Py_Initialize,把 sys.path 指向 bundle 内 pylib 与沙箱数据树,再由 PyQt6 驱动 calibre.gui2.main();QPA 层把 Qt 窗口合成到共享 EGLSurface,菜单与弹窗经 JSON 快照与 TSFN 回调同 ArkTS 双向流转。
text
ArkTS 壳(Index.ets)
├── XComponent SURFACE + splash + 沉浸式避让
├── 选择器桥(QPA pick 请求 → TSFN → DocumentViewPicker → uri 中转)
├── IME 桥(离屏 1×1 TextInput 增量提交 / 退格 / 回车成键)
├── 菜单桥(QMenuBar JSON 快照轮询 → bindMenu → triggerMenuAction 回注)
└── libairshelfbridge.so(薄 NAPI,dlopen 拓扑序)
├── libqohos.so(AirCad Qt6 QPA:EGL 上屏 / 输入注入 / 选择器 helper / IME 上下文)
├── libairshelf_core.so(Qt 宿主线程内 Py_Initialize)
│ └── libpython3.14.so + pylib/(PyQt6、lxml、Pillow、apsw、calibre_extensions)
│ └── calibre.gui2.main()(QApplication 与 calibre 主窗口)
└── BACK 路由(core 3-bit overlay 状态位 → Escape 投递 → onBackPress 消费)
主要目录如下(与仓库实际结构一致):
text
AirShelf/
├── PORTING.md # 补丁清单 / 风险登记册 / 踩坑记录
├── AppScope/app.json5 # com.airshelf.app
├── entry/src/main/
│ ├── module.json5 # deviceTypes: phone/tablet/2in1
│ ├── ets/pages/Index.ets # XComponent 宿主与全部壳层桥
│ ├── ets/utils/FileManager.ets # 选择器中转 / Download 授权 / 导出目录
│ ├── cpp/
│ │ ├── napi_init.cpp # 薄 NAPI 桥
│ │ ├── airshelf_core.cpp # Py_Initialize / BACK 路由 / 诊断通道
│ │ └── qt_platform/ # libqohos 预编译重链通道
│ ├── libs/arm64-v8a/ # 全栈 .so + pylib/(含 .so 的 Python 包)
│ └── resources/rawfile/ # python-data.tar(stdlib + calibre 源码 + 汉化)
└── native/
├── env.sh # NDK + 三借用栈(Qt / 图像XML / zlib png icu)
├── sources/ # Python-3.14.7 / calibre / libffi-3.4.7
├── scripts/ # build_python / build_pydeps / build_pyqt6 /
│ # build_calibre_ext / deploy.sh / fix_needed.py 等
└── staging/ # 中间产物(libffi 等)
▍四、核心适配过程
### 1. CPython 3.14.7 交叉编译:Air 系首个 Python 运行时
native/scripts/build_python.sh 以 python.org 源码为输入,configure --host 加 CONFIG_SITE 覆盖交叉环境下无法执行的检查项,_freeze_module 走 host 编译器,失败模块逐个 --disable,最终产出 libpython3.14.so 与全部 C 扩展。ssl、sqlite3、bz2、lzma 等前置库由 build_pydeps.sh 统一备料。风险登记册 R1 预判的难点全部按预案落地,这一步让 calibre 的全部 Python 代码无需任何改写即可在目标架构解释执行。
### 2. PyQt6 from source 与 .so/.py 双通道分发
PyQt6 的绑定层由 build_pyqt6.sh 以 host venv 里的 sip 驱动 sip-build --qmake,指向 AirGis 栈的交叉 qmake,生成的 Makefile 自动使用 NDK 编译器,这是 PyQt6 在鸿蒙上的首次实证。分发侧受沙箱规则约束:files/ 目录禁止 .so 可执行映射,因此一切含 .so 的 Python 包(PyQt6、calibre_extensions、lxml、Pillow、pypdfium2 等)走 bundle 通道打进 entry/libs/arm64-v8a/pylib/,纯 Python 代码连同资源与 zh_CN 汉化打进 rawfile 的 python-data.tar,首启幂等解包到沙箱,sys.path 同时注入两条通道。
### 3. calibre 树内补丁体系
对 calibre 的全部适配以 PATCH-AIRSHELF-N 标记落在源码树内:WebEngine 缺席守卫让 viewer/preview 路径弹提示而非崩溃;冷启动路径跳过单实例锁、DBus 通知与信号 wakeup fd;USB/MTP 设备层禁用;捐赠与联网功能分两期全量移除(动作不装载、调度定时器消失、菜单项与按钮删除、INTERNET 权限从 module.json5 移除);品牌统一为 AirShelf;fix_html 收口到单 libxml2 副本。每条补丁在 PORTING.md 中都有对应的复现与验收记录。
### 4. 壳层三件套与 BACK 键路由
桌面交互逐一鸿蒙化:菜单栏经 C++ 侧 Qt 元对象导出 JSON 快照(无需 GIL),ArkTS 以 bindMenu 官方组件渲染;文件选择经 qohosfiledialoghelper 转 DocumentViewPicker,uri 结果中转进沙箱;IME 由 QPA 内建 inputcontext 承接,ArkTS 侧以离屏 TextInput 桥增量提交。BACK 键走三层路由:core 用 3-bit 状态位跟踪"弹出菜单/模态对话框/对话框型窗口",airshelf_core_back_key() 按优先级投递 Escape,ArkTS onBackPress 依返回值决定消费或放行系统默认,裸主窗时 BACK 正常退后台。
### 5. download 存储模式与诊断基建
书库默认落在 Download/com.airshelf.app/ 授权目录:启动期 DocumentSaveOptions 的 DOWNLOAD 模式静默取授权,Python 侧写探针实测后建库,存量沙箱书库一次性迁移;toadd/ 自动添加目录让用户从文件管理器或 PC 投书即自动入库,导出经 Exports 子目录落到文件管理器可见位置。诊断侧形成 db-probe/ext-probe/dumpDiagnostics 组合:启动探针验证 sqlite 后端与关键扩展导入,hilog 通道在沙箱 hdc 不可读的设备上转出 Python 栈,devecocli log --crash 作为原生崩溃最终取证。
▍五、适配中遇到的几个难点
### 难点1:双 libxml2 副本的"杂交树"段错误
华为审核报 DOCX 转换闪退,cppcrash 栈稳定落在 xmlRemoveID ← xmlAddIDSafe ← xmlSetNsProp。根因是进程内同时存在两份 libxml2:html5_parser 的 C 扩展动态链接一份,lxml 的 musllinux wheel 静态内嵌另一份;html5_parser 建树后交 lxml 接管,返回的是跨副本杂交对象,lxml 改写属性时命中另一副本的 ID 表即段错误。修复分两步:gui2/comments_editor.py 的 fix_html 弃用 html5_parser 改走 lxml 单副本;bootstrap 启动期把 html5_parser.parse 整体替换为纯 lxml 实现,全调用面一次覆盖,并立下"树内新代码不得再 import html5_parser 建树"的纪律。
### 难点2:musl ELF 纪律与收集链漏洞
musl 的 dlopen 严格匹配无版本号文件名,部署集里 libz.so.1 这类 NEEDED 会直接悬空;fix_needed.py 负责去版本号,但早期收集规则只认 .so 结尾,pillow.libs 整目录的带版本号库形态漏扫,导致 PIL 四个子模块 import 失败。修复是把收集规则扩展到 ^lib.+\.so\.[0-9],libffi 后续又因未入收集规则而复发一次,最终以 staging 三名全收收口。审计方法是全树 llvm-nm --undefined-only 与部署集求差,这一纪律此后贯穿每一轮部署。
### 难点3:窗口几何、输入映射与弹窗语义
超屏对话框按屏幕居中产生负原点几何,渲染侧夹取进 surface 而输入侧按未裁剪几何换算,形成"点谁开谁偏一位"的系统性错位,根修在 QPA 的 setGeometry 对非主窗一律 intersected 到 surface 矩形。QMenu 闪现即关有两层根因:其一触摸按下阶段弹出的菜单被同手势抬起关闭,以 popup 代次计数配合抬起事件改投解决;其二 PC 触控板 tap 双通道投递(触摸回调与鼠标回调各一份),以事件源过滤 TOUCHPAD/MOUSE 源解决。两案共同教训是注入工具验证与真实硬件通道存在差异,输入层验收必须覆盖各设备形态的真实输入。
### 难点4:IME 文本投递的三段修复
对话框内输入框收不到 IME 文本,根因是 Qt 对对话框从不主动激活窗口,focusWindow 恒为主窗,QInputMethodEvent 全部投错目标,修复为对话框 setVisible 时主动 requestActivateWindow;搜索框输入"1"变六个"1",根因是受控 TextInput 的状态从未变化导致重渲染不触发、累计全文反复提交,修复为镜像字段内容做增量 diff,前缀缩短转退格;软键盘退格对 Qt 无效,新增 commitImeKey 接通 QPA 既有按键通道;单行编辑框的回车按文本提交不触发 returnPressed,改为投递 Key_Return。
### 难点5:子进程假设的进程内化
calibre 的转换、页数统计、元数据读取都默认跑在子进程里,隔离是天然的;进程内多线程后,全局状态互踩成家族性问题:page_count 照抄子进程卫生措施导致全局临时目录基址被连坐删除;CurrentDir 的 save/chdir/restore 在多线程交错时互相覆盖 cwd,需引入全局 RLock 收编全部裸 os.chdir;首转重量级模块并发 import 成环死锁,以启动期单线程预热解决;多 worker 并发转换在 GIL 与本地库终结器交叉下冻结,最终 worker_limit=1 串行近似。这一族修复的共性是:凡是上游"进程边界兜底"的代码,进程内都要重审一遍。
▍六、编译、安装与启动
构建分 Native 交叉编译与 HAP 打包两段。Native 侧按 native/scripts/ 顺序执行:
bash
cd AirShelf
source native/scripts/env.sh # NDK + 三借用栈
bash native/scripts/build_python.sh # CPython 3.14.7 交叉
bash native/scripts/build_pydeps.sh # openssl/sqlite/lxml/Pillow/apsw 等
bash native/scripts/build_pyqt6.sh # PyQt6 sip-build 交叉
bash native/scripts/build_calibre_ext.sh # calibre_extensions 子集
bash native/scripts/deploy.sh # 收集 .so → entry/libs(含 pylib/)+ 重打 python-data.tar
随后标准 hvigor 打包:
bash
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk
hvigorw --mode module -p module=entry assembleHap --no-daemon
安装启动(真机调试部署使用 debug 签名产物,release 产物只走发布通道):
bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airshelf.app
PORTING.md 记载的真机验证覆盖 MateBook Pro(PC 首建库全流程、48 本十格式语料批量转换全过)、MatePad Mini(净装冷启动、转换、阅读器、download 书库跨卸载存活)、MatePad 11.5 与 Mate 60 Pro,验证记录含逐轮截图、stderr 与崩溃档案比对。
▍七、当前功能边界
当前已覆盖的能力(均有代码与验收记录对应):
- 书库管理:系统选择器导入(含内容魔数校验拒收伪装文件)、自动添加目录投书入库、删除、编辑元数据、保存到硬盘导出到真实可见目录;
- 格式转换:docx/txt(含 GBK)/epub/mobi/azw3/fb2/rtf/odt/html/md 等输入,PDF 输入走 pypdfium2 纯 Python 引擎,互联网真实语料 48 本十格式在 PC 全过;
- 内置阅读器:EPUB/TXT 章节导航、翻页、字号调节、阅读位置持久化;
- 进程内 E-book 编辑器:文件树、源码编辑、保存回写;
- 全中文界面、强制浅色、download 书库跨卸载存活、首启旧库直读;
- 启动期诊断探针与 hilog 取证通道。
尚未进入当前产物的能力:QtWebEngine 系功能(原版 viewer 独立进程、Live CSS、Inspector、TOC 定位预览等,均有意降级)、USB/MTP 设备传输(整层禁用)、联网生态(商店/新闻抓取/内容服务器/捐赠,决策性移除)、书行长按右键菜单桥接(遗留观察项)、Esc 关闭对话框后偶发的陈旧画面残影(遗留观察项,交互即恢复)。
因此,当前版本更准确的定位是"calibre 的鸿蒙本地书库管理与转换阅读基础版":库管理、格式转换、阅读、编辑四条主线在真机上闭环,上游的重型生态(WebEngine、设备层、联网)以明确移除代替隐性残缺。
▍八、总结
AirShelf 的意义超出单个应用的可用性:它把 CPython 交叉编译、PyQt6 源码绑定、Python 包双通道分发、进程内子进程语义重建这一整套"动态语言桌面栈"的移植方法,第一次在鸿蒙上跑成了完整链路。calibre 这个体量的应用能在平板与 PC 上建库、转换、阅读,说明这条路径对后续任何 Python 系软件都具有复用价值。
后续工作重心在两处:一是收口遗留观察项,包括长按菜单桥接、陈旧画面残影与部分对话框的布局适配;二是按 Phase B 的窗口模型规划继续完善真·多实例窗口治理。每轮变更继续遵循既有纪律:补丁带编号、验收留截图与日志、提审产物内容级 grep 验证。
■ 应用:AirShelf(from Calibre)(AirShelf)
■ 定位:Calibre 移植,电子书库管理
■ 上游:calibre 9.13.0 | 路线:Qt6+Python 运行时 | 版本:1.1.0
■ 开源: https://gitcode.com/Andy88/Harmony-calibre
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917615794843877527
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-calibre
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirShelf
calibre 是开源世界中最完整的电子书库管理软件:书库编目、元数据编辑、格式转换、设备传输、内置阅读器与编辑器,全部围绕一个本地书库组织。对经常与 epub、mobi、azw3、txt 打交道的读者来说,它长期是桌面端不可绕开的基础设施。AirShelf 把 calibre 9.13.0 完整带到鸿蒙侧,选择的是一条此前 Air 系列从未走过的路:不重写、不仿制,直接把这套 1456 个 Python 模块与数十个 C 扩展组成的桌面栈,连同一个完整的 CPython 运行时,交叉编译进 arm64-v8a 的鸿蒙应用沙箱。
这个工程的独特价值在于检验了"动态语言大型桌面应用"的移植路径。Qt Widgets 类 C++ 项目在 Air 系列已有成熟范式,而 calibre 的难点叠加了一层:PyQt6 的 sip 绑定要针对目标平台从源码构建,Python 解释器要以 libpython3.14.so 形态嵌入应用进程,上千个纯 Python 模块还要解决随包分发与运行时定位。这些环节一旦打通,后续任何 Python 系桌面软件的鸿蒙移植都有据可依。
工程概况:包名 com.airshelf.app(AppScope/app.json5),版本 1.0.0,deviceTypes 覆盖 phone、tablet、2in1;目标 HarmonyOS 6.1(API 23)以上,Native ABI 为 arm64-v8a;内核采用 Air 系 Qt6 范式(Qt 6.11.1 预编译栈 + ohos QPA + 薄 NAPI 桥 + 重型 core),叠加 Air 系首个 Python 运行时。PORTING.md 以 PATCH-AIRSHELF-N 编号登记全部树内补丁与踩坑对策,是本文事实的主要来源。
▍二、先划清边界:不把编译通过当成适配完成
calibre 的桌面形态建立在一组鸿蒙侧不存在的底座上:X11/Wayland 窗口系统、QtWebEngine(Chromium)、独立进程的阅读器与编辑器、USB/MTP 设备层、以及默认可用的联网生态。把这些全部照搬既不可行也不必要,工程的处理方式是逐项裁决:能进程内重建的重建(阅读器、编辑器),必须依赖系统沙箱能力替代的替代(文件选择器、IME、书库目录),与移动形态冲突或有害的明确移除(捐赠、联网商店、新闻抓取、内容服务器)。
层次 原项目实现(传统桌面方案) 鸿蒙侧当前方案
启动入口 calibre 启动器拉起 Python 进程 EntryAbility + Index.ets XComponent,NAPI 桥启动 core
GUI 栈 PyQt6 + X11/Wayland 窗口系统 PyQt6 + 自研 ohos QPA(libqohos.so,单 EGLSurface)
Python 运行时 系统或官方 CPython 安装 CPython 3.14.7 交叉编译,libpython3.14.so 随 HAP 分发
内置阅读器 QtWebEngine 独立 viewer 进程 进程内 gui2/airshelf_reader.py(QTextBrowser 实现)
书籍编辑器 ebook-edit 独立进程 进程内 gui2/airshelf_editor.py 顶层窗口
菜单/对话框 原生 QMenuBar / QFileDialog QMenuBar JSON 快照 + ArkTS bindMenu;qohosfiledialoghelper + DocumentViewPicker
设备传输 USB/MTP(udisks) 整层禁用(PATCH-AIRSHELF-3)
联网能力 商店/新闻/内容服务器/捐赠 全量移除(PATCH-AIRSHELF-15 与后续各轮)
书库存储 ~/Calibre Library Download/com.airshelf.app/ 授权目录,跨卸载存活(PATCH-AIRSHELF-34)
边界的意义在于每个降级都有明确编号与验收记录:QtWebEngine 不可交叉(Chromium)对应 PATCH-AIRSHELF-1 的优雅降级守卫;viewer 源码不随包分发,双击书行的默认动作改为编辑元数据,阅读入口接管为自研阅读器。当前产物是可安装、可建库、可转换、可阅读的完整应用,而不是 calibre 界面的静态演示。
▍三、鸿蒙版的整体架构
运行时自顶向下分四层。ArkTS 壳负责 Ability 生命周期、XComponent SURFACE、系统选择器与 IME 桥、菜单栏快照轮询以及 rawfile 数据幂等部署;薄 NAPI 桥只链系统库,按拓扑序 dlopen 依赖栈后进入核心;核心线程内完成 Py_Initialize,把 sys.path 指向 bundle 内 pylib 与沙箱数据树,再由 PyQt6 驱动 calibre.gui2.main();QPA 层把 Qt 窗口合成到共享 EGLSurface,菜单与弹窗经 JSON 快照与 TSFN 回调同 ArkTS 双向流转。
text
ArkTS 壳(Index.ets)
├── XComponent SURFACE + splash + 沉浸式避让
├── 选择器桥(QPA pick 请求 → TSFN → DocumentViewPicker → uri 中转)
├── IME 桥(离屏 1×1 TextInput 增量提交 / 退格 / 回车成键)
├── 菜单桥(QMenuBar JSON 快照轮询 → bindMenu → triggerMenuAction 回注)
└── libairshelfbridge.so(薄 NAPI,dlopen 拓扑序)
├── libqohos.so(AirCad Qt6 QPA:EGL 上屏 / 输入注入 / 选择器 helper / IME 上下文)
├── libairshelf_core.so(Qt 宿主线程内 Py_Initialize)
│ └── libpython3.14.so + pylib/(PyQt6、lxml、Pillow、apsw、calibre_extensions)
│ └── calibre.gui2.main()(QApplication 与 calibre 主窗口)
└── BACK 路由(core 3-bit overlay 状态位 → Escape 投递 → onBackPress 消费)
主要目录如下(与仓库实际结构一致):
text
AirShelf/
├── PORTING.md # 补丁清单 / 风险登记册 / 踩坑记录
├── AppScope/app.json5 # com.airshelf.app
├── entry/src/main/
│ ├── module.json5 # deviceTypes: phone/tablet/2in1
│ ├── ets/pages/Index.ets # XComponent 宿主与全部壳层桥
│ ├── ets/utils/FileManager.ets # 选择器中转 / Download 授权 / 导出目录
│ ├── cpp/
│ │ ├── napi_init.cpp # 薄 NAPI 桥
│ │ ├── airshelf_core.cpp # Py_Initialize / BACK 路由 / 诊断通道
│ │ └── qt_platform/ # libqohos 预编译重链通道
│ ├── libs/arm64-v8a/ # 全栈 .so + pylib/(含 .so 的 Python 包)
│ └── resources/rawfile/ # python-data.tar(stdlib + calibre 源码 + 汉化)
└── native/
├── env.sh # NDK + 三借用栈(Qt / 图像XML / zlib png icu)
├── sources/ # Python-3.14.7 / calibre / libffi-3.4.7
├── scripts/ # build_python / build_pydeps / build_pyqt6 /
│ # build_calibre_ext / deploy.sh / fix_needed.py 等
└── staging/ # 中间产物(libffi 等)
▍四、核心适配过程
### 1. CPython 3.14.7 交叉编译:Air 系首个 Python 运行时
native/scripts/build_python.sh 以 python.org 源码为输入,configure --host 加 CONFIG_SITE 覆盖交叉环境下无法执行的检查项,_freeze_module 走 host 编译器,失败模块逐个 --disable,最终产出 libpython3.14.so 与全部 C 扩展。ssl、sqlite3、bz2、lzma 等前置库由 build_pydeps.sh 统一备料。风险登记册 R1 预判的难点全部按预案落地,这一步让 calibre 的全部 Python 代码无需任何改写即可在目标架构解释执行。
### 2. PyQt6 from source 与 .so/.py 双通道分发
PyQt6 的绑定层由 build_pyqt6.sh 以 host venv 里的 sip 驱动 sip-build --qmake,指向 AirGis 栈的交叉 qmake,生成的 Makefile 自动使用 NDK 编译器,这是 PyQt6 在鸿蒙上的首次实证。分发侧受沙箱规则约束:files/ 目录禁止 .so 可执行映射,因此一切含 .so 的 Python 包(PyQt6、calibre_extensions、lxml、Pillow、pypdfium2 等)走 bundle 通道打进 entry/libs/arm64-v8a/pylib/,纯 Python 代码连同资源与 zh_CN 汉化打进 rawfile 的 python-data.tar,首启幂等解包到沙箱,sys.path 同时注入两条通道。
### 3. calibre 树内补丁体系
对 calibre 的全部适配以 PATCH-AIRSHELF-N 标记落在源码树内:WebEngine 缺席守卫让 viewer/preview 路径弹提示而非崩溃;冷启动路径跳过单实例锁、DBus 通知与信号 wakeup fd;USB/MTP 设备层禁用;捐赠与联网功能分两期全量移除(动作不装载、调度定时器消失、菜单项与按钮删除、INTERNET 权限从 module.json5 移除);品牌统一为 AirShelf;fix_html 收口到单 libxml2 副本。每条补丁在 PORTING.md 中都有对应的复现与验收记录。
### 4. 壳层三件套与 BACK 键路由
桌面交互逐一鸿蒙化:菜单栏经 C++ 侧 Qt 元对象导出 JSON 快照(无需 GIL),ArkTS 以 bindMenu 官方组件渲染;文件选择经 qohosfiledialoghelper 转 DocumentViewPicker,uri 结果中转进沙箱;IME 由 QPA 内建 inputcontext 承接,ArkTS 侧以离屏 TextInput 桥增量提交。BACK 键走三层路由:core 用 3-bit 状态位跟踪"弹出菜单/模态对话框/对话框型窗口",airshelf_core_back_key() 按优先级投递 Escape,ArkTS onBackPress 依返回值决定消费或放行系统默认,裸主窗时 BACK 正常退后台。
### 5. download 存储模式与诊断基建
书库默认落在 Download/com.airshelf.app/ 授权目录:启动期 DocumentSaveOptions 的 DOWNLOAD 模式静默取授权,Python 侧写探针实测后建库,存量沙箱书库一次性迁移;toadd/ 自动添加目录让用户从文件管理器或 PC 投书即自动入库,导出经 Exports 子目录落到文件管理器可见位置。诊断侧形成 db-probe/ext-probe/dumpDiagnostics 组合:启动探针验证 sqlite 后端与关键扩展导入,hilog 通道在沙箱 hdc 不可读的设备上转出 Python 栈,devecocli log --crash 作为原生崩溃最终取证。
▍五、适配中遇到的几个难点
### 难点1:双 libxml2 副本的"杂交树"段错误
华为审核报 DOCX 转换闪退,cppcrash 栈稳定落在 xmlRemoveID ← xmlAddIDSafe ← xmlSetNsProp。根因是进程内同时存在两份 libxml2:html5_parser 的 C 扩展动态链接一份,lxml 的 musllinux wheel 静态内嵌另一份;html5_parser 建树后交 lxml 接管,返回的是跨副本杂交对象,lxml 改写属性时命中另一副本的 ID 表即段错误。修复分两步:gui2/comments_editor.py 的 fix_html 弃用 html5_parser 改走 lxml 单副本;bootstrap 启动期把 html5_parser.parse 整体替换为纯 lxml 实现,全调用面一次覆盖,并立下"树内新代码不得再 import html5_parser 建树"的纪律。
### 难点2:musl ELF 纪律与收集链漏洞
musl 的 dlopen 严格匹配无版本号文件名,部署集里 libz.so.1 这类 NEEDED 会直接悬空;fix_needed.py 负责去版本号,但早期收集规则只认 .so 结尾,pillow.libs 整目录的带版本号库形态漏扫,导致 PIL 四个子模块 import 失败。修复是把收集规则扩展到 ^lib.+\.so\.[0-9],libffi 后续又因未入收集规则而复发一次,最终以 staging 三名全收收口。审计方法是全树 llvm-nm --undefined-only 与部署集求差,这一纪律此后贯穿每一轮部署。
### 难点3:窗口几何、输入映射与弹窗语义
超屏对话框按屏幕居中产生负原点几何,渲染侧夹取进 surface 而输入侧按未裁剪几何换算,形成"点谁开谁偏一位"的系统性错位,根修在 QPA 的 setGeometry 对非主窗一律 intersected 到 surface 矩形。QMenu 闪现即关有两层根因:其一触摸按下阶段弹出的菜单被同手势抬起关闭,以 popup 代次计数配合抬起事件改投解决;其二 PC 触控板 tap 双通道投递(触摸回调与鼠标回调各一份),以事件源过滤 TOUCHPAD/MOUSE 源解决。两案共同教训是注入工具验证与真实硬件通道存在差异,输入层验收必须覆盖各设备形态的真实输入。
### 难点4:IME 文本投递的三段修复
对话框内输入框收不到 IME 文本,根因是 Qt 对对话框从不主动激活窗口,focusWindow 恒为主窗,QInputMethodEvent 全部投错目标,修复为对话框 setVisible 时主动 requestActivateWindow;搜索框输入"1"变六个"1",根因是受控 TextInput 的状态从未变化导致重渲染不触发、累计全文反复提交,修复为镜像字段内容做增量 diff,前缀缩短转退格;软键盘退格对 Qt 无效,新增 commitImeKey 接通 QPA 既有按键通道;单行编辑框的回车按文本提交不触发 returnPressed,改为投递 Key_Return。
### 难点5:子进程假设的进程内化
calibre 的转换、页数统计、元数据读取都默认跑在子进程里,隔离是天然的;进程内多线程后,全局状态互踩成家族性问题:page_count 照抄子进程卫生措施导致全局临时目录基址被连坐删除;CurrentDir 的 save/chdir/restore 在多线程交错时互相覆盖 cwd,需引入全局 RLock 收编全部裸 os.chdir;首转重量级模块并发 import 成环死锁,以启动期单线程预热解决;多 worker 并发转换在 GIL 与本地库终结器交叉下冻结,最终 worker_limit=1 串行近似。这一族修复的共性是:凡是上游"进程边界兜底"的代码,进程内都要重审一遍。
▍六、编译、安装与启动
构建分 Native 交叉编译与 HAP 打包两段。Native 侧按 native/scripts/ 顺序执行:
bash
cd AirShelf
source native/scripts/env.sh # NDK + 三借用栈
bash native/scripts/build_python.sh # CPython 3.14.7 交叉
bash native/scripts/build_pydeps.sh # openssl/sqlite/lxml/Pillow/apsw 等
bash native/scripts/build_pyqt6.sh # PyQt6 sip-build 交叉
bash native/scripts/build_calibre_ext.sh # calibre_extensions 子集
bash native/scripts/deploy.sh # 收集 .so → entry/libs(含 pylib/)+ 重打 python-data.tar
随后标准 hvigor 打包:
bash
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk
hvigorw --mode module -p module=entry assembleHap --no-daemon
安装启动(真机调试部署使用 debug 签名产物,release 产物只走发布通道):
bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airshelf.app
PORTING.md 记载的真机验证覆盖 MateBook Pro(PC 首建库全流程、48 本十格式语料批量转换全过)、MatePad Mini(净装冷启动、转换、阅读器、download 书库跨卸载存活)、MatePad 11.5 与 Mate 60 Pro,验证记录含逐轮截图、stderr 与崩溃档案比对。
▍七、当前功能边界
当前已覆盖的能力(均有代码与验收记录对应):
- 书库管理:系统选择器导入(含内容魔数校验拒收伪装文件)、自动添加目录投书入库、删除、编辑元数据、保存到硬盘导出到真实可见目录;
- 格式转换:docx/txt(含 GBK)/epub/mobi/azw3/fb2/rtf/odt/html/md 等输入,PDF 输入走 pypdfium2 纯 Python 引擎,互联网真实语料 48 本十格式在 PC 全过;
- 内置阅读器:EPUB/TXT 章节导航、翻页、字号调节、阅读位置持久化;
- 进程内 E-book 编辑器:文件树、源码编辑、保存回写;
- 全中文界面、强制浅色、download 书库跨卸载存活、首启旧库直读;
- 启动期诊断探针与 hilog 取证通道。
尚未进入当前产物的能力:QtWebEngine 系功能(原版 viewer 独立进程、Live CSS、Inspector、TOC 定位预览等,均有意降级)、USB/MTP 设备传输(整层禁用)、联网生态(商店/新闻抓取/内容服务器/捐赠,决策性移除)、书行长按右键菜单桥接(遗留观察项)、Esc 关闭对话框后偶发的陈旧画面残影(遗留观察项,交互即恢复)。
因此,当前版本更准确的定位是"calibre 的鸿蒙本地书库管理与转换阅读基础版":库管理、格式转换、阅读、编辑四条主线在真机上闭环,上游的重型生态(WebEngine、设备层、联网)以明确移除代替隐性残缺。
▍八、总结
AirShelf 的意义超出单个应用的可用性:它把 CPython 交叉编译、PyQt6 源码绑定、Python 包双通道分发、进程内子进程语义重建这一整套"动态语言桌面栈"的移植方法,第一次在鸿蒙上跑成了完整链路。calibre 这个体量的应用能在平板与 PC 上建库、转换、阅读,说明这条路径对后续任何 Python 系软件都具有复用价值。
后续工作重心在两处:一是收口遗留观察项,包括长按菜单桥接、陈旧画面残影与部分对话框的布局适配;二是按 Phase B 的窗口模型规划继续完善真·多实例窗口治理。每轮变更继续遵循既有纪律:补丁带编号、验收留截图与日志、提审产物内容级 grep 验证。
楼主
8 天前