从 noexec 沙箱到鸿蒙应用市场:筑眸的 CPython 3.12 常驻内核实践
从 noexec 沙箱到鸿蒙应用市场:筑眸的 CPython 3.12 常驻内核实践
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_BlenderBIM
应用市场里的一个详情页只有一屏:图标、名称、三张预览图、一段简介、一个「安装」按钮。可要让这个按钮真正变成用户桌面上一个能打开 IFC 建筑模型的应用,背后要落定的,是一整条从进程、动态库、文件协议、权限、隐私、签名到包体的工程链路。本文以上架视角回看筑眸的 HarmonyOS PC 移植:先看货架上的样子,再一层层拆开货架底下的技术。
| 项目 | 上架信息 / 工程事实 |
|---|---|
| 应用名 | 筑眸 · ZhuMou BIM |
| 上架版本 | 1.0.0 (1000000),详情页标注 2026/11/5 |
| 包名 | org.zhumu.bim |
| 分类 / 分级 / 设备 | 工具 / 3+ / 电脑(HarmonyOS PC、2in1、tablet) |
| 商店简介关键词 | ArkTS 宿主 + 常驻 Python 内核、ArkUI、socketpair、CPython 3.12、模型一次解析多次查询、全程本地运行、不联网、零权限申请 |
| 内核与运行时 | 纯 Python IFC SPF 解析内核 + CPython 3.12 aarch64-linux-ohos |
| 权限 | requestPermissions: [] |
| 开源许可 | GPL-3.0 |

一、上架现场:先把四张图摆出来
图 1 · 应用市场详情页

图 1:详情页给出「筑眸 · 鸿蒙PC上的建筑模型浏览器」,分类「工具」、年龄「3+」、标签「电脑」、版本 1.0.0 (1000000) 与 2026/11/5 的更新记录;简介写着「ArkTS 宿主 + 常驻 Python 内核」混合架构、ArkUI 绘制全部界面、socketpair 驱动 CPython 3.12、模型一次解析多次查询、全程本地运行、不联网、零权限申请;页面底部还有独立的「App 隐私」条目,指向开发者隐私政策。
这一屏其实已经是上架验收单:应用身份(图标 / 名称 / 包名)、版本(versionCode)、分级与分类、宣传截图、功能描述、隐私披露,缺一项都走不到「安装」。更微妙的是,简介里那句「零权限申请」不是文案修辞,它要求工程里真的没有一张权限表可填——否则用户点开权限详情就会穿帮。
图 2 · 安装后首次启动:隐私声明先行

图 2:主界面已经在身后(深色三栏、工具栏与控制台都在),前景是系统/分发侧统一呈现的「关于筑眸与隐私的声明」:说明服务与个人信息处理范围、目的与方式,用户点「同意」后才进入应用,也可「取消」。这不是应用自绘的对话框——源码里没有这个弹窗的 ArkTS 实现——而是上架后由平台侧按商店配置的隐私声明呈现。
这条边界对移植工程师很重要:隐私合规不等于在应用里画一个同意按钮。它是「商店侧声明 + 系统侧呈现 + 应用侧最小数据处理」三件事的组合。筑眸在应用侧能做的、也必须做的,是不申请权限、不联网、只在用户主动选择后把文件读进自己的沙箱。
图 3 · 真正跑起来的模型浏览器

图 3:空间结构树展开到 PROJECT / SITE / BUILDING,平面视图画出两室小屋的房间填充、墙体、门窗与尺寸线;右侧属性面板显示 IFCSPAC· 起居室 的 GlobalId、Name、ObjectPlacement、Representation、LongName;右下角控制台能看到 load_sample 返回的内核路径 /data/storage/el2/base/haps/entry/files/ohos_python/lib/python3.12/site-packages/samples/sample-house.ifc。
这张图是给审核与用户看的「功能实证」:不是启动页,不是占位图,而是一次真实解析的结果。控制台里那条沙箱路径同时说明了上架包的运行形态——CPython 标准库与内核被解压在应用私有目录里,而不是系统全局 Python。
图 4 · 关于对话框:把技术身份写在应用里

图 4:筑眸 / ZhuMou BIM、鸿蒙 PC 上的原生 IFC 模型浏览器:打开 .ifc 文件,浏览空间结构,查看平面视图与构件属性;版本 1.0.0、构建 bonsai-1.0.0-p1、内核 IfcOpenShell / BlenderBIM、运行时 CPython 3.12 · aarch64-linux-ohos、架构 ArkTS 宿主 + 常驻 Python 内核、平台 HarmonyOS NEXT · API 26、开源许可 GPL-3.0。
把「关于」页做进应用,是上架型移植最划算的一笔投资:版本、构建号、许可、平台一眼可查,客服与审核不必反编译 HAP 就能核对;开源许可写清楚,也避免 GPL-3.0 的合规争议。
四张图全部放在文章前段,后面就是它们底下的东西:身份、签名、包体、隐私、功能完整性、文案边界与可复现源码。
二、为什么「上架」会反过来约束移植架构
桌面开源软件的默认假设是「用户自己装依赖、自己给权限、自己看日志」。应用市场把这三件事全部收走了:依赖必须随包、权限必须最小、崩溃必须可诊断但不能把用户丢进黑屏。于是上架要求会倒推架构决策:
| 上架要求 | 对移植的倒推 | 筑眸的选择 |
|---|---|---|
| 不能要求用户装 Python | 解释器必须随 HAP 分发 | libpython3.12.so 放 libs/arm64-v8a,标准库随 rawfile 解压到 filesDir |
| 不能申请一堆存储权限 | 文件访问必须用户授权制 | requestPermissions: [] + DocumentViewPicker |
沙箱 noexec、禁止自由 fork/exec | 子进程必须用平台 API | childProcessManager.startNativeChildProcess + dlopen + Py_BytesMain |
| 审核会点每一个按钮 | 不能有「预留功能」「空菜单」 | 工具栏只保留 4 个都有真实实现的按钮 |
| 隐私披露要可核对 | 数据处理范围要窄且可说明 | 不联网、不上传,用户模型只复制进应用私有目录 |
| 崩溃不能白屏 | 异常要在跨语言边界收敛 | Python dispatch 捕获一切异常返回 JSON,ArkTS bridge 再兜一层 |
| 升级与签名可验证 | 包名与 profile 必须绑定,版本递增 | org.zhumu.bim + versionCode 1000000,签名材料不入库 |
| 包体不能失控 | 二进制资产要有取舍 | collectAllLibs 只收 .so,运行时 zip 随包一次分发,扫描产物不进 libs |
| 开源许可可追溯 | 应用内与仓库内声明一致 | About 页、LICENSE、oh-package.json5 统一 GPL-3.0 |
换句话说,上架不是移植完成后的行政步骤,而是从第一天就写进 module.json5 和 CMakeLists.txt 的约束。下面按这条链路逐段展开。
三、身份与版本:从 app.json5 到商店页
应用身份的第一现场是 AppScope/app.json5:
{
"app": {
"bundleName": "org.zhumu.bim",
"vendor": "zhumu",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}
versionName 1.0.0 与详情页的「版本 1.0.0 (1000000)」严格对应;versionCode 是升级判断依据,下一次提包必须递增,不能只改展示用的版本号。bundleName 是签名 profile 的绑定对象,一旦更换包名就要重新走签名与上架流程。
显示名走资源,而不是硬编码:
// entry/src/main/resources/base/element/string.json
{"string": [
{"name": "module_desc", "value": "Bonsai native entry"},
{"name": "EntryAbility_desc", "value": "Bonsai ability"},
{"name": "EntryAbility_label", "value": "筑眸"}
]}
设备形态与权限表在 entry/src/main/module.json5:
{
"module": {
"name": "entry",
"type": "entry",
"mainElement": "EntryAbility",
"deviceTypes": ["2in1", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"label": "$string:EntryAbility_label",
"exported": true,
"skills": [
{ "entities": ["entity.system.home"], "actions": ["action.system.home"] }
]
}
],
"requestPermissions": []
}
}
deviceTypes 决定它出现在「电脑 / 平板」的货架上,requestPermissions: [] 决定商店的权限披露能保持干净。
应用内的「关于」页用同一组常量,避免商店、HAP、About 三处版本打架:
// entry/src/main/ets/pages/Index.ets
const BUILD_TAG = 'bonsai-1.0.0-p1';
const APP_VERSION = '1.0.0';
@Builder
infoRow(label: string, value: string) {
Row() {
Text(label).fontSize(13).fontColor('#7f8c99').width(76)
Text(value).fontSize(13).fontColor('#e6edf3').layoutWeight(1)
}.width('100%')
}
// AboutDialog.build() 内部
this.infoRow('版本', APP_VERSION)
this.infoRow('构建', BUILD_TAG)
this.infoRow('内核', 'IfcOpenShell / BlenderBIM')
this.infoRow('运行时', 'CPython 3.12 · aarch64-linux-ohos')
this.infoRow('架构', 'ArkTS 宿主 + 常驻 Python 内核')
this.infoRow('平台', 'HarmonyOS NEXT · API 26')
this.infoRow('开源许可', 'GPL-3.0')
四、签名与包体:能装、能升级、能过审
4.1 签名是「身份 + 版本」的最后一道校验
HarmonyOS 的 profile 与 bundleName 绑定。同系列鸿蒙 PC 移植工程有过一条很典型的教训:把 AppGallery 的 release profile 直接拿去 hdc install,会报 9568322 (not trusted app source)——release 包只能走应用市场通道,本地真机验证要换与包名匹配的 debug 材料。另一个常见变体是受限 ACL 权限没写进 profile,安装时报 9568289。对上架型项目,这两条应写进提包前的自查表,而不是等 rejecting 邮件来了再查。
公开源码仓里的 build-profile.json5 是净化后的版本,没有任何证书、p12 路径或密码:
{
"app": {
"products": [
{
"name": "default",
"compatibleSdkVersion": "6.0.1(21)",
"targetSdkVersion": "6.0.2(22)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"nativeLib": {
"collectAllLibs": true,
"debugSymbol": { "strip": false }
}
}
}
],
"buildModeSet": [ { "name": "debug" }, { "name": "release" } ]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [ { "name": "default", "applyToProducts": ["default"] } ]
}
]
}
本地出包时由 DevEco 自动签名把 signingConfigs 写回本机文件;提交仓库前清空它,是上架工程最基本的卫生习惯。.gitignore 里也要把凭据挡在门外:
# ---- signing material ----
*.p12
*.p7b
*.cer
*.keystore
.ohos/
# ---- native prebuilt libs & runtime (large binaries, not source) ----
entry/libs/
**/*.zip
**/*.so
# ---- build output ----
build/
entry/build/
.hvigor/
.idea/
.codebuddy/
4.2 包体不能为了「小」而把运行时裁坏
collectAllLibs: true 让 entry/libs/arm64-v8a 里的 libpython3.12.so 与全部 *.cpython-312-aarch64-linux-ohos.so 打进 HAP;strip: false 是红线——被 strip 的预编译 libpython 在 musl dlopen 阶段会直接 SIGSEGV。Native 层甚至为此写了崩溃信号诊断,把原因写进 bim_srv_boot.txt:
void OnFatalSignal(int sig)
{
const char *name = (sig == SIGSEGV) ? "SIGSEGV" : ((sig == SIGABRT) ? "SIGABRT" : "FATAL");
if (g_crashOutFd >= 0) {
WriteAll(g_crashOutFd,
std::string("[embed] FATAL ") + name +
" during dlopen/run (often stripped libpython). rebuild with nativeLib.debugSymbol.strip=false\n");
}
if (g_crashExitFd >= 0) WriteAll(g_crashExitFd, "139\n");
_exit(139);
}
包体构成上,本地工程目录约 286 MB,其中绝大多数是构建产物:3 个 .hap 合计 114 MB、2 个 .app 合计 36.6 MB、entry/libs 与 build/intermediates 里的 .so 约 54 MB、运行时 zip 8.3 MB 的两份副本。真正进入上架包的是签名后的单个 HAP(本地 entry-default-signed.hap 约 38.6 MB)。因此包体治理的原则不是「删功能」,而是:
entry/libs/arm64-v8a只放.so,任何扫描产物、说明文件都会被collectAllLibs一并打包;- 运行时 zip 只保留
rawfile里的一份,构建中间目录不随仓库分发; - 示例 IFC 是 11 KB 级文本,随包分发远比塞一张位图划算。
4.3 命令行提包
$env:DEVECO_SDK_HOME = "C:\Program Files\Huawei\DevEco Studio\sdk"
$env:JAVA_HOME = "C:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH = "C:\Program Files\Huawei\DevEco Studio\jbr\bin;$env:PATH"
& "C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat" `
--mode module -p module=entry@default -p product=default assembleHap --no-daemon
jbr/bin 必须前置到 PATH:PackageHap 阶段会 spawn java 调打包工具,缺失时报 00308018;DEVECO_SDK_HOME 也必须显式设置,命令行构建不会自动继承 IDE 配置。
五、隐私与权限:商店页那行「App 隐私」背后
详情页的「App 隐私」条目指向开发者隐私政策;首次启动的同意弹窗由平台侧按这份声明呈现(图 2)。应用侧要做的,是让声明里的每一句话都经得起核对。
一、不申请权限。
"requestPermissions": []
没有存储权限、没有网络权限、没有后台保活。这句空数组是「零权限申请」文案的全部依据。
二、只在用户主动选择后读文件。
private async openIfcFile(): Promise<void> {
if (this.busy || !this.context) return;
const docPicker = new picker.DocumentViewPicker(this.context);
const opts = new picker.DocumentSelectOptions();
opts.fileSuffixFilters = ['.ifc'];
const uris = await docPicker.select(opts);
if (!uris || uris.length === 0) return; // 用户取消,什么也不做
const dest = `${this.filesDir}/opened_model.ifc`;
const srcFile = fileIo.openSync(uris[0], fileIo.OpenMode.READ_ONLY);
const dstFile = fileIo.openSync(
dest, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.TRUNC);
const CHUNK = 1024 * 1024;
const buf = new ArrayBuffer(CHUNK);
while (true) {
const n = fileIo.readSync(srcFile.fd, buf);
if (n <= 0) break;
fileIo.writeSync(dstFile.fd, buf, { offset: 0, length: n });
}
fileIo.closeSync(dstFile);
fileIo.closeSync(srcFile);
await this.bridge('open_path', JSON.stringify([dest]));
}
用户不选,应用碰不到任何文档;选了也只把副本写进 filesDir/opened_model.ifc,不长期持有原始 URI。
三、不联网、不上传。 运行时、示例模型、内核全部随 HAP 分发;解析在设备端 Python 子进程内完成;Native 子进程只访问 filesDir 下的请求/响应与模型文件;内核的 exec_script 端点未暴露到 UI。
四、异常不穿透。 上架应用最怕「点一下就没了」,所以跨语言边界上所有异常都收敛成数据:
def dispatch(fn, args=None):
global _DOC, _DOC_PATH
args = args or []
try:
# load_sample / open_path / get_tree / get_entity / get_stats / get_geometry ...
return json.dumps(result)
except Exception:
import traceback
return json.dumps({"ok": False, "error": traceback.format_exc(limit=5)})
private async bridge(fn: string, argsJson: string): Promise<string> {
try {
return await this.kernelRequest(fn, argsJson, 60000);
} catch (e) {
const msg = (e as Error).message ?? String(e);
this.uiLog(`[kernel] 请求失败 (${fn}): ${msg}`);
return JSON.stringify({ ok: false, error: msg });
}
}
五、生命周期干净。 aboutToDisappear 关闭全部 fd,killKernel 写 bim_srv_stop.txt 让 Python 内核自行退出,finally 删除 READY 哨兵文件。应用被划走或关闭时,不留常驻进程、不留临时请求文件。
| 隐私自查项 | 工程依据 |
|---|---|
| 权限最小化 | requestPermissions: [] |
| 用户授权访问 | DocumentViewPicker.select(),取消即返回 |
| 数据本地化 | 模型副本落在 filesDir,解析在设备端 |
| 不联网 | 无网络权限、无网络代码路径 |
| 不长期持有 | 不持久化用户原始 URI |
| 可说明 | 商店隐私政策 + 首启声明 + About 页身份信息 |
| 可清理 | 关闭模型 / 停止内核 / 删除临时 req/resp 文件 |
六、功能完整性:审核看的是每个按钮都真的能用
同系列鸿蒙 PC 移植工程有过一次很值得记的审核打回:Qt 版应用里「工具」菜单会去 QProcess 启动 HAP 中不存在的外部子工具,弹「无法启动程序」;「文档」页 10 个分类里 9 个永远为空,双击看起来像「点不开」。审核结论只有四个字:功能异常。教训是桌面软件移植时,那些「预留」「暂未适配」的入口,在应用市场里都会被当成缺陷。
筑眸据此把工具栏收敛成四个按钮,每一个都有完整实现与可见反馈:
Button('示例模型')
.enabled(this.booted && !this.busy)
.onClick(() => { void this.loadSample(); })
Button('打开 IFC…')
.enabled(this.booted && !this.busy)
.onClick(() => { void this.openIfcFile(); })
Button('关闭模型')
.enabled(this.booted && this.modelLoaded && !this.busy)
.onClick(() => { void this.closeModel(); })
Button('关于')
.onClick(() => { this.aboutDialog.open(); })
enabled 绑定 booted / busy / modelLoaded,内核还没就绪时按钮不可点,避免用户点出一串超时;加载完成后才有「关闭模型」可点。
一次「示例模型」点击,会走完三段内核调用并把结果分别反映到树、统计栏与画布:
private async afterModelOpen(filename: string, entityCount: number, parseMs: number): Promise<void> {
this.modelLoaded = true;
this.modelTitle = filename;
this.uiLog(`[模型] ${filename} — ${entityCount} 实体, 解析 ${parseMs} ms`);
await this.refreshTree(); // fn=get_tree
await this.refreshStats(); // fn=get_stats
await this.refreshGeometry(); // fn=get_geometry
}
空状态也不是空白:Canvas 在无模型时绘制提示文字,模型树显示「先加载模型」,属性面板显示「在模型树中选择构件」,控制台始终有 [内核] 就绪(常驻进程)。这四句话,就是审核点开应用还没加载模型时看到的全部内容——每一处都有明确状态,而不是疑似卡死。
双向点选同样要能被点出来:
Canvas(this.canvasCtx)
.onReady(() => { this.renderViewport(); })
.onClick((evt: ClickEvent) => { this.pickShapeAt(evt.x, evt.y); })
pickShapeAt 把组件内坐标反算成世界坐标,用 point-in-polygon 命中构件;日志会把「画布坐标 → 世界坐标 → 命中类型」打进控制台。可观测性在这里不只是给开发者看的,也是「这个功能确实执行了」的证据。
真机验收数据:sample-house.ifc → IFC4 · 266 实体 · 5 墙体 · 2 房间 · 2 门 · 2 窗,解析 8 ms,12 个构件轮廓;树、属性/Pset、平面图纸、画布点选、关闭重启全部通过。
七、产品文案与工程实现的边界
商店简介是面向用户的概括表述,里面有一句:
「底层经 socketpair 通道驱动 CPython 3.12 运行 IfcOpenShell 解析引擎,模型一次解析、多次查询,全程本地运行、不联网、零权限申请。」
前半句描述的是架构与数据模型归属,后半句(一次解析、本地、离线、零权限)则与工程实现逐字对应。这里必须补一句工程上的诚实说明:
P1 交付的内核并不是 IfcOpenShell 的 C++ B-Rep 几何引擎,而是本工程自研的、零 C 依赖的纯 Python ISO-10303-21(SPF)解析器与平面轮廓投影,语义上对齐 BlenderBIM / IfcOpenShell 的 IFC 数据模型。真正交叉编译 OCCT / IfcOpenShell 到 aarch64-linux-ohos 属于 P2。应用「关于」页写「内核 IfcOpenShell / BlenderBIM」与工具栏副标题同理,都是对上游语义基准的致敬,而不是声称已移植 C++ 内核。
这不是要追究文案,而是上架型项目应有的分层:
| 场合 | 允许的表达 | 要求 |
|---|---|---|
| 商店简介 | 面向用户概括能力与体验 | 不得宣称不存在的能力(如 3D 编辑、联网同步) |
| About 页 | 写清版本、构建、平台、许可 | 与 HAP 常量一致,便于核对 |
| README / 技术文档 | 精确到「哪一层是自己写的、哪一层还没做」 | 明确 P1/P2/P3 边界 |
| 开源仓 | 代码与声明一致 | GPL-3.0、签名材料不入库、二进制资产说明 |
建议后续版本把商店简介里的「IfcOpenShell 解析引擎」调整为「IFC 语义内核」,把 P2 完成后的真实 B-Rep 能力再写进简介——这样产品文案与工程实现始终同频,也避免开源社区按图索骥却在仓库里找不到 IfcOpenShell 的 C++ 源码。
八、上架前自检清单
| # | 检查项 | 依据 / 落点 |
|---|---|---|
| 1 | 包名与签名 profile 绑定 | org.zhumu.bim;release 包走应用市场,本地验证用匹配 debug 材料 |
| 2 | 版本号与 versionCode 一致且递增 | 1.0.0 / 1000000,与商店页、About 一致 |
| 3 | 设备形态正确 | deviceTypes: ["2in1","tablet"],商店标签「电脑」 |
| 4 | 权限表为空、隐私政策已配置 | requestPermissions: [];详情页「App 隐私」条目 |
| 5 | 首启声明与用户同意 | 平台侧呈现(图 2),取消不进入应用 |
| 6 | 图标、名称、分类、分级 | 工具 / 3+ / 筑眸 |
| 7 | 预览图是真实运行画面 | 图 3、图 4 一类实拍,不用效果图占位 |
| 8 | 每个按钮都有实现 | 示例模型 / 打开 IFC / 关闭模型 / 关于 |
| 9 | 空状态有提示、不白屏 | 树、画布、属性、控制台四处提示 |
| 10 | 崩溃不白屏、错误可观察 | dispatch / bridge 双层兜底 + 控制台日志 |
| 11 | 运行时随包、首启可解压 | rawfile/bonsai_python_runtime.zip + .READY_B1 marker |
| 12 | 原生库未被 strip | debugSymbol.strip: false,否则 dlopen SIGSEGV |
| 13 | libs 目录只含 .so | 避免 collectAllLibs 打包扫描产物 |
| 14 | 不联网、不上传 | 无权限、无网络代码路径 |
| 15 | 开源许可一致 | GPL-3.0:LICENSE / oh-package / About / 仓库 |
| 16 | 签名凭据不入库 | 清空 signingConfigs,.gitignore 排除 p12/p7b/cer |
| 17 | 源码仓可复现 | 见下一节 |
| 18 | 资产清理 | 关闭模型释放文档、停内核、关 fd、删 req/resp |
九、源码仓与可复现
上架之后把源码放进公开仓,是这类移植项目最有价值的一步:审核可核对、社区可复现、许可可追溯。
仓库:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_BlenderBIM,main 分支 1aa4243(基于仓库初始提交 458945c 线性提交)。
git clone https://atomgit.com/OpenHarmonyPCDeveloper/ohos_BlenderBIM.git
cd ohos_BlenderBIM
仓内 46 个文件的边界很清楚:
ohos_BlenderBIM/
├── AppScope/app.json5 # 身份与版本
├── build-profile.json5 # SDK / nativeLib(签名段已清空)
├── entry/src/main/ets/pages/Index.ets # ArkTS 单页面:UI + 协议 + Canvas
├── entry/src/main/cpp/ # bonsai_bridge / bonsai_child / CMake
├── entry/src/main/module.json5 # 2in1 / tablet、零权限
├── python_kernel/ # 纯 Python IFC 内核 + 构建脚本 + 示例 IFC + schema
├── README.md / README.OpenHarmony_CN.md
├── LICENSE # GPL-3.0 全文
└── .gitignore # 排除二进制与签名材料
有两点必须写在明处,否则「可复现」会变成「跑不起来」:
- 二进制运行时未收录:
entry/libs/arm64-v8a/*.so(交叉编译 CPython 与 C 扩展)与rawfile/bonsai_python_runtime.zip属预编译产物,不在仓库中;克隆后需自备aarch64-linux-ohos的 CPython 3.12 运行时,按python_kernel/build_runtime.py生成 zip 放入rawfile/,脚本里的BUILD、SRC_ZIP、LIBS_DIR、RAWFILE路径常量按本机调整。 - 本机签名需重建:仓库不含
signingConfigs,用 DevEco 打开工程自动签名即可;release 包只能走应用市场通道,真机调试请换与包名匹配的 debug 材料。
复现命令(在自备运行时之后):
$env:DEVECO_SDK_HOME = "C:\Program Files\Huawei\DevEco Studio\sdk"
$env:JAVA_HOME = "C:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH = "C:\Program Files\Huawei\DevEco Studio\jbr\bin;$env:PATH"
& "C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat" `
--mode module -p module=entry@default -p product=default assembleHap --no-daemon
内核自检与重打包:
python python_kernel/check_server.py # 校验 PY_SERVER 内嵌 Python 源码语法
python python_kernel/test_engine.py # 宿主侧内核逻辑自检
python python_kernel/build_runtime.py # 重打运行时 zip
# 内容变更时同步递增 Index.ets 的 RUNTIME_MARKER
十、踩坑与经验
| 现象 / 风险 | 根因 | 上架视角的处理 |
|---|---|---|
| 首启隐私弹窗被误认为应用自绘 | 平台侧按商店隐私声明统一呈现 | 应用侧别再画一份重复弹窗;把精力放在零权限与最小数据处理 |
9568322 not trusted app source | release profile 直接装真机 | release 走市场;本地验证换绑定同包名的 debug 材料 |
9568289 安装失败 | profile 缺受限 ACL | 自动签名时在 DevEco 里确认权限被写进 profile |
dlopen(libpython) SIGSEGV | 预编译库被 strip | debugSymbol.strip: false + Native 层崩溃诊断 |
| HAP 体积莫名膨胀 | libs 目录混入非 .so 文件 | 该目录只放 .so,扫描产物移出 |
| 点按钮没反应但无报错 | 共享请求文件被排队请求覆盖 | 每请求独立 bim_req_<seq>/bim_resp_<seq> 文件对 + 串行队列 |
| 改了内核但设备没变化 | marker 未变,复用旧解压目录 | 递增 RUNTIME_MARKER |
| 运行时解压目录错乱 | Compress-Archive 写反斜杠路径 | 用 tar -a -cf 打包 |
| 商店文案与仓库实现不符 | 概括表述被当成实现声明 | 文档里明确 P1 纯 Python 内核、P2 才引入 C++ B-Rep |
| 审核判「功能异常」 | 预留菜单、空分类、外部子工具 | 只保留有完整实现的按钮,空状态给明确提示 |
| 提交仓库时带出 p12 密码 | 本机签名材料未清理 | 清空 signingConfigs + .gitignore 双保险 |
还有一条跨项目的经验值得留下:上架包与源码仓是两种交付物。前者追求「用户装上就能用」,必须把运行时、示例、图标、许可全部塞进 HAP;后者追求「别人能看清并复现」,必须把二进制、凭据、构建缓存全部剔除。混在一起,要么仓库巨大且泄密,要么克隆后跑不起来。
十一、QA 章节
| # | 问题 | 回答 |
|---|---|---|
| Q1 | 上架的是哪个版本? | 应用市场详情页显示 1.0.0 (1000000),更新记录 2026/11/5;构建号 bonsai-1.0.0-p1,平台 HarmonyOS NEXT · API 26。 |
| Q2 | 零权限为什么还能打开本地 IFC? | requestPermissions 为空数组,文件由 DocumentViewPicker 经用户主动选择后授权读取,1 MB 分块复制进 filesDir 再解析。用户取消就什么都不做。 |
| Q3 | 图 2 的隐私弹窗是应用自己写的吗? | 不是。源码中没有该弹窗的 ArkTS 实现,它由平台/分发侧按商店配置的隐私声明在首启时统一呈现;应用侧负责的是最小数据处理与隐私政策可核对。 |
| Q4 | 为什么 HAP 里要带一个 CPython? | filesDir 是 noexec,不能 exec 一个 python3;方案是 libpython3.12.so 放 libs,标准库随 zip 解压到 filesDir,由 Native 子进程 dlopen 后调 Py_BytesMain。 |
| Q5 | socketpair 在架构里做什么? | 沙箱 posix_openpt 返回 EPERM,用 socketpair(AF_UNIX, SOCK_STREAM) 传 fd 给子进程并接管 stdio;真正的请求/响应是带 seq 的 JSON 文件对,便于排队、重试与排障。 |
| Q6 | 上架包和本地调试包的签名有什么区别? | 签名 profile 绑定 org.zhumu.bim;release 包走应用市场通道,直接 hdc install 会报 9568322,真机调试需换匹配的 debug 材料。仓库不含任何签名凭据。 |
| Q7 | 为什么不把 libpython strip 小一点? | 被 strip 的预编译 libpython 在 musl dlopen 阶段会 SIGSEGV,启动即崩。包体优化只能从「libs 只放 so」「不打包中间产物」入手。 |
| Q8 | 商店说 IfcOpenShell 解析引擎,仓库里怎么没有 C++ 内核? | P1 是零 C 依赖的纯 Python SPF 解析与平面轮廓投影,语义对齐 BlenderBIM / IfcOpenShell;交叉编译 OCCT / IfcOpenShell 是 P2。技术文档里已明确这一边界,建议后续把商店文案改为「IFC 语义内核」。 |
| Q9 | 应用联网吗?会把模型传走吗? | 不联网。无网络权限、无网络代码路径;运行时与示例全部随包,解析在设备端 Python 子进程完成,模型副本只在应用私有目录。 |
| Q10 | 用户打开的模型最终存在哪? | filesDir/opened_model.ifc;示例模型位于解压后的 ohos_python/lib/python3.12/site-packages/samples/(图 3 控制台可见该路径)。关闭模型会释放文档并清理临时请求文件。 |
| Q11 | 审核被打回过吗? | 就本项目公开记录而言,详情页已显示 1.0.0 上架。同系列鸿蒙 PC 移植工程曾因「工具菜单起不存在的子程序 / 文档分类恒空」被判功能异常,因此筑眸按第 6 章逐项自查所有按钮、空状态与日志。 |
| Q12 | 商店的「App 隐私」条目要怎么补齐? | 应用侧保持零权限与离线;分发侧在商店配置隐私政策与数据处理说明,首启声明由平台呈现;应用内 About 页写清版本、平台与许可,三者互相印证。 |
| Q13 | 克隆仓库能直接构建出可安装包吗? | 源码齐全,但二进制运行时(libs 下的 .so 与 rawfile 的 zip)未收录,需自备 aarch64-linux-ohos CPython 3.12 产物并按 python_kernel/build_runtime.py 生成;签名在 DevEco 里重新自动签名。 |
| Q14 | 「关于」页为什么值得放进上架包? | 版本、构建、内核、运行时、平台、许可一眼可查,审核与客服无需反编译 HAP;GPL-3.0 声明也随应用分发,合规成本极低。 |
| Q15 | 后续版本最该做什么? | P2 交叉编译 IfcOpenShell / OCCT 替换纯 Python 几何;P3 3D 视口、剖切量测与图层过滤;同时把商店文案同步到真实能力边界。 |
十二、结语
上架页面上的「安装」按钮很轻,轻到用户不会想到它下面压着一个 noexec 的沙箱、一对 socketpair、一队带序号的 JSON 文件、一个常驻的 CPython 解释器,以及一份必须能被逐字核对的隐私声明。
技术移植的终点从来不是「能跑」,而是「能被别人安全地装上运行、能说清自己做了什么、能被复现、能被继续维护」。筑眸把 BlenderBIM / IFC 的世界带到 HarmonyOS PC 上,前一半工作量在读懂那份 #1=IFCPROJECT(...) 的文本,后一半工作量,就藏在应用市场那一屏简洁的详情页里。
更多推荐



所有评论(0)