从 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 筑眸在 HarmonyOS 应用市场的详情页

图 1:详情页给出「筑眸 · 鸿蒙PC上的建筑模型浏览器」,分类「工具」、年龄「3+」、标签「电脑」、版本 1.0.0 (1000000) 与 2026/11/5 的更新记录;简介写着「ArkTS 宿主 + 常驻 Python 内核」混合架构、ArkUI 绘制全部界面、socketpair 驱动 CPython 3.12、模型一次解析多次查询、全程本地运行、不联网、零权限申请;页面底部还有独立的「App 隐私」条目,指向开发者隐私政策。

这一屏其实已经是上架验收单:应用身份(图标 / 名称 / 包名)、版本(versionCode)、分级与分类、宣传截图、功能描述、隐私披露,缺一项都走不到「安装」。更微妙的是,简介里那句「零权限申请」不是文案修辞,它要求工程里真的没有一张权限表可填——否则用户点开权限详情就会穿帮。

图 2 · 安装后首次启动:隐私声明先行

图 2 点击「打开」后首次启动呈现的隐私声明

图 2:主界面已经在身后(深色三栏、工具栏与控制台都在),前景是系统/分发侧统一呈现的「关于筑眸与隐私的声明」:说明服务与个人信息处理范围、目的与方式,用户点「同意」后才进入应用,也可「取消」。这不是应用自绘的对话框——源码里没有这个弹窗的 ArkTS 实现——而是上架后由平台侧按商店配置的隐私声明呈现。

这条边界对移植工程师很重要:隐私合规不等于在应用里画一个同意按钮。它是「商店侧声明 + 系统侧呈现 + 应用侧最小数据处理」三件事的组合。筑眸在应用侧能做的、也必须做的,是不申请权限、不联网、只在用户主动选择后把文件读进自己的沙箱。

图 3 · 真正跑起来的模型浏览器

图 3 示例模型加载后选中 IFCSPACE 起居室

图 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 关于对话框:版本、构建、内核、运行时、架构、平台与许可

图 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子进程必须用平台 APIchildProcessManager.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原生库未被 stripdebugSymbol.strip: false,否则 dlopen SIGSEGV
13libs 目录只含 .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                          # 排除二进制与签名材料

有两点必须写在明处,否则「可复现」会变成「跑不起来」:

  1. 二进制运行时未收录: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 路径常量按本机调整。
  2. 本机签名需重建:仓库不含 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 sourcerelease profile 直接装真机release 走市场;本地验证换绑定同包名的 debug 材料
9568289 安装失败profile 缺受限 ACL自动签名时在 DevEco 里确认权限被写进 profile
dlopen(libpython) SIGSEGV预编译库被 stripdebugSymbol.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。
Q5socketpair 在架构里做什么?沙箱 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(...) 的文本,后一半工作量,就藏在应用市场那一屏简洁的详情页里。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐