Skip to content

Latest commit

 

History

History
940 lines (775 loc) · 45.9 KB

File metadata and controls

940 lines (775 loc) · 45.9 KB

App Packaging

最后更新:2026-08-10;适用版本:0.6.0-dev;兼容基线:0.5.0

JellyFrame app packaging 会把 web-like 源文件转成确定性的、适合固件集成的 app 资源。 这里不应照搬手机或手表应用商店的安装包;JellyFrame 更适合保留小型类 Web 的开发体验, 然后在桌面离线生成无需文件系统、网络栈或复杂压缩归档即可加载的资源。

部署形态应分成两类:

  • 静态 C++ 资源表用于固件内置 app:启动器、表盘、软件列表、出厂 app、bring-up fixture 和安全兜底 UI。
  • Flash 中的可安装 .jfapp bundle 才是第三方 app 的目标路径。V0 bundle 已能由桌面工具生成, 并可由 Win32 壳和 CLI preview path 直接加载。如果第三方 app 必须靠重新烧录固件安装,JellyFrame 相比传统 LVGL/C 固件 UI 的核心优势会消失。
  • 启动器应被建模为带系统权限的 JellyFrame App 角色,而不是 runtime 内置页面。仓库提供 samples/apps/system/sample_launcher 作为桌面 bring-up/CI 样例;产品宿主可以选择自己的 受信 launcher app。

调研摘要

几个成熟平台有明显共性:

  • Xiaomi Vela JS app 使用 src/manifest.json 描述应用身份、版本、权限、系统配置和页面路由。 项目源码位于 src/,包含 app.ux、页面文件、公共资源和 i18n 文件,构建后输出包产物。
  • Zepp OS 小程序使用根目录 app.json 描述 app 元信息、runtime 要求、权限、targets 和 i18n。 不同设备的资源放在按 targets 命名的 assets 子目录中。
  • HarmonyOS 应用包比 JellyFrame 需要的复杂得多,但它仍体现了 app 级配置、module 级配置和资源的分层。
  • Android App Bundle 是发布格式,不是设备直接安装的运行时格式。它对 JellyFrame 最有价值的启发是: 保留完整源包,再按目标设备生成更小的运行包。
  • Apple bundle 使用标准目录层级和类似 Info.plist 的元信息,让代码能稳定找到资源。
  • MSIX 使用显式 manifest 和文件映射/完整性元数据。完整性思路有价值,但容器和安装模型对 MCU 太重。

设计目标

  • 源包对 Web 作者友好:HTML、CSS、classic JS、assets、fonts 和 i18n 文件。
  • 构建输出可重复:资源排序、路径规范化、稳定生成 C++ table、debug 目录或 binary installable bundle。
  • 运行时加载保持 O(log n) 或小包有界线性查找,不做高堆开销的归档解析。
  • 部署前声明全部预算:resource bytes、DOM nodes、CSS rules、display commands、timers、listeners 和 framebuffer policy。
  • 包资源加载继续无文件系统、无网络;宿主通过现有 HostResourceLoader 边界提供包内字节。
  • 应用可以为运行时数据 API 声明网络能力。这个能力与包资源加载分离,因此仍禁止远程页面、 远程 CSS/script/image 资源,但不会把未来宿主提供的 fetch API 路线写死。
  • 可选资源缺失时干净降级;必需 entry 资源缺失应在烧录前由打包器报错。
  • 打包默认执行字体资源预检,报告源码中使用的非 ASCII 字符、推荐 font profile 和 bitmap 字体预算;CLI 默认还会在 report 旁生成 *.used_chars.txt,作为 app 字体补充包的输入。 开发者只有在明确知道目标环境字体覆盖由系统保证时才应使用 --no-font-check--font-subset off

运行时安装与网络边界

JellyFrame 的第三方 app 目标是安装到 flash/外部存储的 bundle,而不是重新烧录固件。静态 C++ 资源表仍然有价值,但它更适合启动器、表盘、系统设置、出厂 app 和安全兜底 UI。 面向 app 作者的启动、挂起、恢复、service completion 和 recovery 语义见 app_lifecycle_zh.md

安装式 bundle 的运行时边界:

  • app manager/system shell 拥有安装、删除、升级和 app 列表刷新。
  • 当前运行 app 的 JavaScript 不能直接挂载新 bundle,也不能修改全局资源索引。
  • 下载、hash/签名校验、manifest/budget 校验和 flash 写入都属于宿主异步 job。
  • 安装完成后通过 completion event 通知 UI,启动器下一帧刷新列表。
  • 失败时 staging 区可直接丢弃,不能破坏已提交的 app 表。
  • 桌面或产品 bring-up 时,宿主可在完成下载和签名校验后,把本地 install-candidate JSON 交给 JellyFrame 工具。V0 schema 位于 tools/schemas/jellyframe.install_candidate.schema.jsonjellyframe_cli.py install --candidate candidate.json 会校验声明的 SHA-256、签名状态、用户批准和 update policy,然后提交引用的本地 .jfapp。JellyFrame 仍不负责网络下载或密码学签名验证。

运行时数据与存储边界:

  • permissions: ["network"] / capabilities: ["network.fetch"] 只声明运行时数据请求。
  • capabilities: ["storage.kv"] 只声明 app 私有小型异步 KV storage。
  • capabilities: ["media.audio.playback"] 声明可选的宿主持有音频播放。当前 runtime 已有 command/handle/completion 验证,Win32 壳可用 --audio-smoke 验证桌面 host adapter 能接收本地或包内音频资源;JerryScript 构建已暴露带 ended/error 状态事件的宿主可选极小 Audio() V0 子集,但仍不内置 MCU codec。
  • capabilities: ["sensor.accelerometer"]["sensor.gyroscope"]["sensor.heart-rate"]["sensor.ambient-light"]["location.position"] 声明语义设备数据需求。它们只允许通过有界 host service 请求小型 sample/snapshot;真实采样频率、后台策略、用户授权和硬件驱动都属于宿主/profile。 App 不会获得裸 GPIO/I2C/SPI/BLE/GPS 句柄。宿主绑定 location service 时,location.position 会启用 navigator.geolocation.getCurrentPosition(...) V0 子集;sensor JavaScript API 仍延后。
  • app 可以请求天气、账号同步、小型 JSON 或下载由系统安装器继续验证的 .jfapp
  • 仍禁止把远程 HTML/CSS/script/image 当作页面资源直接加载。
  • 仍不提供 cookie、浏览器级持久同步 localStorage、IndexedDB、Cache API 或通用文件系统; 只有宿主绑定非阻塞 app 私有 shadow 时才暴露极小 localStorage V0。
  • 通用文件编辑不是 V0 普通 app 能力。未来系统组件、文件管理器或用户批准的 app 应通过 manifest/profile capability 进入宿主持有的 file broker,而不是获得裸 filesystem handle,也不应 增加 JellyFrame 私有 HTML/JS 语法。没有这类授权时,app 不得影响 runtime 文件、系统 app 或其他 app 数据;即使得到授权,每个操作也必须有界异步执行、稳定错误和不依赖重新烧写固件的 fallback。
  • 目标 profile 可以按产品策略拒绝网络/storage/audio/sensor/location app,或限制最大响应字节、并发请求、 超时、KV item 数、byte 配额、stream 数量和未释放 sample/snapshot 数量。

这个设计保留未来“第三方安装式 app”的价值,同时避免 MCU 端变成一个不可控的通用浏览器 loader。

源包目录

推荐源包结构:

my_app/
  jellyframe.app.json
  index.html
  styles/
    app.css
  scripts/
    app.js
  assets/
    icon.png
    images/
  fonts/
    ui.bdf
  i18n/
    en-US.json
    zh-CN.json

只有 jellyframe.app.json 和声明的 entry HTML 是必需的。其他文件均可选,并通过本地绝对路径或相对路径引用。

README.mdREADME_zh.md、隐藏目录、__pycache__.DS_StoreThumbs.db 等开发说明/系统文件会被 packer 忽略。它们可以用于说明源包,而不会扩大运行时资源或污染字体覆盖报告。

Manifest V0

第一版 manifest 应保持很小,并使用 JSON。它由桌面工具消费,而不是由 MCU runtime 解析:

{
  "$schema": "../../../tools/schemas/jellyframe.app.schema.json",
  "format": "jellyframe.app",
  "formatVersion": 0,
  "id": "com.example.weather",
  "name": "Weather",
  "role": "app",
  "version": {
    "name": "0.1.0",
    "code": 1
  },
  "entry": "/index.html",
  "runtime": {
    "minJellyFrame": "0.4.0",
    "script": "classic"
  },
  "viewport": {
    "designWidth": 300,
    "designHeight": 300,
    "shape": "round"
  },
  "budgets": {
    "maxResourceBytes": 131072,
    "maxDomNodes": 512,
    "maxCssRules": 256,
    "maxDisplayCommands": 512
  },
  "fonts": [
    {
      "id": "ui",
      "source": "fonts/ui.bdf",
      "profile": "app-subset-cn",
      "sizes": [16, 20],
      "weights": [400, 700]
    }
  ],
  "targets": {
    "esp32s3-round-300": {
      "viewport": {
        "width": 300,
        "height": 300,
        "shape": "round"
      },
      "fontProfile": "app-subset-cn",
      "output": "cpp"
    }
  },
  "permissions": ["network"],
  "capabilities": ["network.fetch"]
}

schema 位于 tools/schemas/jellyframe.app.schema.json。developer CLI 可以输出 schema 或其路径:

python tools/jellyframe_cli.py schema --print-path

role 默认为 app。当前已保留 launcherwatchfacesettings 等系统角色。声明系统角色或 system.launcher/system.appManager capability 并不会自动获得权限;它只表达 app 的意图, 真正授权必须由宿主/profile 根据签名、安装来源或产品策略决定。

capabilities 接受 JellyFrame 标准能力名,也接受产品私有扩展名。未知名称不再让 schema 直接拒绝 package,但如果所选产品 profile 没有通过 host policy 明确支持同名能力,工具仍会报告 manifest-capability-unknown。可移植 app 应优先使用标准名称;产品私有名称只应保留给无法用已文档化 Web-near 子集描述的服务。

Canvas 2D 的标准能力名是 graphics.canvas2d。它用于声明 app 需要 <canvas> / CanvasRenderingContext2D 这类按需像素绘制能力。Canvas 会消耗 backing store 内存和 JS 绘制时间, 因此它不是默认能力;target profile 会通过 hostServices.canvas2d 报告当前产品是否支持。 运行时支持是可选且有界的:只有 getContext("2d") 后才分配像素,产品应只在内存预算能接受 surface 上限时开启 hostServices.canvas2d。Canvas V0.4 覆盖纯色矩形、有界 path、抗锯齿 stroked line、arcfillglobalAlphasave/restorefontmeasureTextfillText、canvas-to-canvas drawImage() 3/5/9-argument form、像素对齐的 translate/resetTransform、有界二次/三次曲线,以及有界 linear 和两 stop 同心 radial gradient。ImageData、非 canvas image/video source、通用 matrix、scale/rotate 和 pattern fill 仍延后。gradient 文本目前会退化为一个采样色,以便 Canvas 继续复用宿主 text backend。

fonts 是 JellyFrame bitmap font 路径的部署/runtime 声明,不是完整 CSS @font-face 实现。打包器会把 .jffont.bdf.ttf.otf.woff 等文件作为普通 Font 资源写入资源表或 .jfapp。其中 .jffont V0/V1 已可由 runtime 解析并随 app instance 持有;AppFontSet 提供 bitmap fallback chain:generic/未指定 family 的文本先尝试系统字体 profile,再用 app .jffont supplement 补缺字;CSS font-family 的首个自定义 family 若匹配 manifest fonts[].family,则优先尝试对应 app 字体。Win32 壳可用 --use-app-fonts 显式切到这条验收路径。

manifest 中的每个 font 条目可以写:

{
  "id": "ui-cn",
  "source": "fonts/ui-cn.jffont",
  "profile": "app-subset-cn",
  "family": "Jelly UI CN",
  "license": {
    "name": "OFL-1.1",
    "source": "Noto Sans SC subset",
    "url": "https://fonts.google.com/noto/specimen/Noto+Sans+SC"
  },
  "sizes": [16],
  "weights": [400, 700]
}

family 同时用于 package diagnostics 和 runtime app-font 选择。system-uisans-serif 等 generic family 会报告为 generic fallback;未匹配的首选自定义 family 会产生 font-family-unmatched。runtime 匹配刻意很小:只规范化 CSS family list 中首个自定义 family, 并与 manifest .jffont family 匹配;不实现完整浏览器 cascade、@font-face、stretch/style/features 和完整字体匹配。app-font backend 使用便宜的整数倍字号策略:根据 CSS font-size 除以字体 line height 选择 1x/2x/3x... 缩放,并设置上限避免异常字号失控;font-weight >= 600 沿用合成粗体描边。sizesweights 用来声明这个 package font 已验收的 CSS 字号和字重。 工具仍会检查这些数组是否存在且合法, 发布前可报告 font-axis-metadata-missingfont-axis-metadata-invalidlicense.namelicense.source 是推荐字段;缺失时 pack/check 会给出 font-license-missingfont-license-incomplete, 便于发布前确认字体来源。 budgets.maxAppFontsbudgets.maxAppFontBytesbudgets.maxAppFontGlyphs 会限制可用 runtime .jffont 数量、总字节和总 glyph 数;超过时会报告 font-budget-exceeded。这些检查只发生在工具层, 不会增加 MCU 渲染热路径成本。

samples/apps/packages/jelly_font_policy 是这条路径的验收样例:它在 CSS 和 manifest 元数据中分别声明 Jelly Tiny CNJelly Tiny Symbols family,包含由仓库 BDF fixture 生成的极小 .jffont supplement,验收整数倍字号缩放,并故意保留一个缺字探针,方便验证工具 warning 是否稳定。

当前稳定生产路径仍是:打包/检查阶段收集 used chars,离线从授权字体生成 bitmap glyph 数据, 再把生成的 BitmapFont 编译进 port/firmware 或作为 .jffont supplement 随 .jfapp 安装,并通过 TextMeasureProvider/TextPainterAppFontSet 注入。

CLI 的 checkpackagepreview 和源码包 install 默认使用 --font-subset auto。 这会运行字体预检、输出 report.used_chars.txt,并在 JSON report 中写入:

{
  "fontSubset": {
    "mode": "auto",
    "usedChars": "build/my_app.used_chars.txt",
    "generatedFont": "",
    "generated": false
  }
}

如果已有授权 BDF bitmap font,可以让 CLI 在同一次预检中生成 .jffont

python tools\jellyframe_cli.py check `
  --root samples\apps\packages\jelly_font_policy `
  --target round-300 `
  --report build\font_policy.report.json `
  --font-source-bdf src\render_core\samples\fonts\bitmap\tiny.bdf `
  --font-output build\font_policy.generated.jffont `
  --font-coverage-bits 2 `
  --font-allow-missing

生成的 .jffont 不会自动修改源码包。要让 runtime 使用它,仍需把文件复制或输出到 app 目录下,并在 jellyframe.app.jsonfonts[] 中声明 idsourceprofilefamily 和授权信息。这个显式步骤是有意保留的:字体来源和授权必须由 app 作者确认,工具不应替作者 默默发布第三方字体资源。

内置 target presets 位于 tools/presets/targets。可以这样列出:

python tools/jellyframe_cli.py targets

内置 source-package 模板位于 tools/templates/apps。可以这样列出并创建:

python tools/jellyframe_cli.py templates
python tools/jellyframe_cli.py new `
  --template calculator `
  --output build/my_calculator `
  --id org.example.calculator `
  --name Calculator `
  --target round-300

使用 --target id 时,packer 会先加载存在的 tools/presets/targets/id.json,再叠加 manifest 中同名 targets[id] 设置。只存在于 manifest 的自定义 target 也允许; 完全未知的 target 会明确失败。

targets[id].gate 是可选的发布前适配门槛。它只由桌面 CLI 在 responsive profile 检查中消费, 不进入 MCU runtime,也不改变页面行为。示例:

{
  "targets": {
    "round-300": {
      "viewport": { "width": 300, "height": 300, "shape": "round" },
      "output": "jfapp",
      "gate": {
        "policy": "reject",
        "minViewport": { "width": 300, "height": 300 },
        "allowScroll": false,
        "allowHorizontalOverflow": false,
        "maxWarnings": 0,
        "maxErrors": 0
      }
    }
  }
}

policy 决定违反 gate 时 report 写入 acceptwarn 还是 rejectreject 会让 check/package/install 失败;warn 只计入 warning,适合试用期或还在打磨的窄屏 target。当前 gate 可检查最小 viewport、是否允许滚动、是否允许横向溢出,以及 diagnostics warning/error 数。它是发布前适配契约,不是完整 responsive layout 引擎。

Responsive profile 还会携带小型 diagnosticSamples[] 列表。它只保留代表性的 target-specific 诊断,例如窄屏文本溢出,不会把整份伪浏览器日志复制到每个 profile。对常见布局诊断, 样本会尽量带上解析后的 textnodepathmetrics.measuredWidthmetrics.availableWidthmetrics.boxHeightmetrics.contentHeightmetrics.overflowYmetrics.boxOverflowLeftmetrics.boxOverflowRightpaintBoundsviewport 等字段。developerAdvice[] 会消费这些样本,从而把建议指向 具体 target 和最小可行动 detail。部分建议还会携带类似 app_author_recipes.md#scroll-listrecipe 字符串;它只是文档指针,不是运行时能力开关。

影响运行兼容性的字段应由 packer 强制要求:

  • idversion.codeentryruntime.minJellyFrame
  • 全局或 target-specific viewport;
  • budgets.maxResourceBytes
  • target 名称和输出类型。

permissions: ["network"]capabilities: ["network.fetch"] 表示 app 期待未来宿主提供 运行时网络请求 API。它们不会启用远程包资源。packer 会把这些字段写入报告,产品固件可以据此拒绝 不符合板级策略的 app。

运行时应把这些 manifest 字段转换为 AppServiceManifestCapabilities,再与选中的 AppServiceHostProfile 通过 app_service_policies_for_app(...) 合成。某个 capability 被请求, 并不等于已经启用;host/profile 必须同时允许它,并提供有界预算。

资源路径规则

JellyFrame 应使用严格的本地路径子集:

  • 包资源 URL 只能是本地路径:不接受冒号/scheme、不接受 //host、不做 query 驱动的网络 fetch。
  • 绝对 app 路径以 / 开头。
  • 相对路径按引用它的资源目录解析。
  • . 忽略;.. 如果逃出 app root 就拒绝。
  • 拒绝 symlink。packer 在读取字节前也会确认解析后的资源路径仍在 app root 内。
  • 构建工具把分隔符规范化为 /,拒绝重复规范化路径,并按路径排序。
  • CSS url(...)<link href><script src>、图片和字体使用同一个 resolver。
  • budgets.maxResourceBytes 会作为单资源上限执行,同时报告总资源字节数;总量超限会产生 resource-budget-exceeded,方便嵌入式 target 按产品策略警告或拒绝。
  • document 加载时,同一预算也限制已接受 CSS 与 classic-script source 的聚合字节数;宿主还会限制 stylesheet 与 script 的数量。超限时通过 css-document-resource-limitscript-document-resource-limit 跳过后续完整资源,绝不会把截断资源交给 parser 或 script runtime。

这些规则与当前 ESP32-S3 bring-up 接近,也能让 MCU loader 很小。

构建输出

工具可以从同一份 manifest 生成两类输出。

桌面/调试输出:

dist/my_app.jfdir/
  jellyframe.package.json
  resources...

固件内置输出:

dist/my_app_resources.cpp
dist/my_app_resources.h
dist/my_app_font.h
dist/my_app_report.json

第三方安装输出:

dist/my_app.jfapp
dist/my_app_report.json

静态 C++ table、debug 目录和 .jfapp binary bundle 向 runtime 暴露同一张逻辑资源表。 Renderer 和 script runtime 不应关心字节来自编译期 table、flash partition、debug 目录, 还是板级 app store。

生成的资源表应包含:

  • 规范化路径;
  • resource kind;
  • byte pointer 或 blob offset;
  • byte size;
  • 可选 checksum,用于诊断;
  • 可选 path hash,用于快速查找。

小包使用排序后的线性查找已经足够简单。较大包可以使用排序后的 FNV-1a path hash 加字符串确认, 避免 hash 碰撞导致难查的问题。

.jfapp V0 Binary Format

.jfapp V0 是一个小端、未压缩、固定索引的二进制资源包。它不是 ZIP,也不是小型文件系统。 桌面工具可以一次性生成和完整校验;MCU port 可以把它放在 flash/外部存储中,按 offset 读取资源。

文件布局:

header
manifest summary JSON
resource index[]
string table
payload blob

header 字段:

char     magic[8];        // "JFAPPV0\0"
uint16_t header_size;     // 56
uint16_t format_version;  // 0
uint32_t flags;           // V0 must be 0
uint32_t summary_offset;
uint32_t summary_size;
uint32_t index_offset;
uint32_t resource_count;
uint32_t string_table_offset;
uint32_t string_table_size;
uint32_t payload_offset;
uint32_t payload_size;
uint32_t bundle_crc32;    // crc32 with this field zeroed
uint32_t reserved;        // V0 must be 0

每个资源索引条目固定 28 字节:

uint32_t path_hash;       // FNV-1a over normalized UTF-8 app path
uint32_t path_offset;     // offset into string table
uint16_t path_size;
uint16_t kind;            // 0 other, 1 css, 2 classic js, 3 image, 4 font
uint32_t payload_offset;  // offset into payload blob
uint32_t payload_size;
uint32_t crc32;           // resource bytes
uint32_t flags;           // V0 must be 0

V0 明确不做压缩、加密、动态 native code、远程资源、依赖包和 MCU 端 JSON 全量解析。 manifest summary 是 packer 生成的紧凑 JSON,桌面工具和调试壳可读取;真正的嵌入式安装器可以 只解析需要的固定字段或使用打包报告/registry 生成的摘要。

.jffont V0/V1 Binary Font Supplement

.jffont 是面向 .jfapp 动态字体补充的第一版二进制容器。它不是 TTF/OTF/WOFF 加载器,也不要求 MCU 端运行矢量字体 rasterizer;它保存的是已离线裁剪和 rasterize 的 bitmap glyph。 V0 保存紧凑的 1bpp glyph rows。V1 保持同一个固定表结构,并增加显式 2bpp/4bpp coverage rows, 用于字体级抗锯齿。当前工具可以生成该文件,render core 已提供只读内存 view 解析器,app runtime 也有按 app_instance_id 绑定的 AppFontSet 用于持有 .jffont bytes 并在 app 退出/切换时释放。 第一版 bitmap fallback chain 已实现:系统字体优先,然后 app supplement 补缺。CSS/runtime 激活语义和 产品级 text backend 选择仍在主线 C 中继续实现。

文件布局:

header
glyph index[]
bitmap rows blob

header 固定 32 字节,小端:

char     magic[8];          // "JFFONT0\0"
uint16_t header_size;       // 32
uint16_t format_version;    // 0 为 1bpp,1 为 coverage glyph
uint32_t glyph_count;
uint8_t  line_height;
uint8_t  fallback_advance;
uint16_t flags;             // V0: 0,V1: 低 8 位为 coverage bits 1/2/4
uint32_t glyph_table_offset;
uint32_t row_data_offset;
uint32_t row_data_size;

每个 glyph index 条目固定 16 字节,并按 Unicode codepoint 升序排列:

uint32_t codepoint;
uint32_t row_offset;        // offset into bitmap rows blob
uint32_t row_size;
uint8_t  width;
uint8_t  height;
uint8_t  advance;
uint8_t  bytes_per_row;

这基本复用 BitmapFontGlyph/BitmapFont 的数据模型,但去掉了 C++ 指针和编译期符号。 BitmapFontResource 可以把 .jffont bytes 解析成只读 view,并复用现有二分 codepoint lookup 和 bitmap painter。Coverage glyph 是显式的画质/存储取舍:2bpp 大致让 row data 翻倍, 4bpp 大致让 row data 变为四倍;绘制时会对每个覆盖 glyph 像素多做一次小型 alpha coverage blend。 使用 V0/1bpp 字体的 app 不支付这部分成本。当前生成器从 BDF bitmap 邻近像素近似生成 coverage, 它不是运行时矢量 rasterizer,也不承诺浏览器级 subpixel typography。低层 view 要求调用方保证 .jffont bytes 在 view 生命周期内保持有效; AppFontSet::load_jffont 会为当前 app instance 持有一份 bytes 拷贝,适合桌面/source-package 安全路径;AppFontSet::attach_jffont_viewAppRuntimeHost::attach_current_jffont_view 可直接挂载稳定的 flash/mmapped 或 .jfapp bundle payload,不复制字体 bytes。Win32 壳对可安装 .jfapp bundle 使用该 view 路径,对源目录仍回退到复制路径。 当前 jellyframe_font_pack_gen 支持同时生成固件静态 header 和 .jffont

jellyframe_font_pack_gen `
  --bdf app_font.bdf `
  --chars used_chars.txt `
  --output app_font.h `
  --output-binary app_font.jffont `
  --coverage-bits 4 `
  --name app_font

打包流水线

推荐桌面构建链:

  1. 校验 jellyframe.app.json 并规范化路径。
  2. 遍历 entry HTML、linked stylesheets 和 classic scripts。
  3. 运行真实管线组件上报的 diagnostics。诊断应来自真正执行了 parse、CSS、style、layout、 layer、render、scripting 或 package loading 的组件。
  4. 默认执行字体资源预检:收集源码字符、估算 bitmap font budget,并在提供 --font-coverage 时验证目标字体覆盖。需要产出嵌入式 C++ 字体头文件时,再使用 jellyframe_font_pack_gen
  5. 执行预算检查:单资源字节、总包字节、CSS rule 估算和 script byte 限制。
  6. 为选定 target 生成资源表。
  7. 输出报告:warnings、管线 diagnostics、可选字体覆盖、估算内存和最终资源表。

明确裁剪

嵌入式包模型刻意不包含:

  • MCU 端 runtime ZIP 解压或通用归档解析;
  • 依赖包管理器;
  • 动态 native libraries;
  • 任意远程包资源;
  • service workers、caches 或浏览器存储;
  • 多进程安装/更新语义;
  • 类似 Vela .ux 或 HarmonyOS ArkUI 的页面模块转译。

这些功能应放在 embedded core 之外,或等未来桌面 packaging 层需要时再做。

当前工具

推荐开发者入口是 tools/jellyframe_cli.py。仅校验:

python tools/jellyframe_cli.py validate `
  --root samples/apps/packages/watch_weather `
  --report build/watch_weather_report.json

生成 C++ resource table 和 report:

python tools/jellyframe_cli.py package `
  --root samples/apps/packages/watch_weather `
  --target round-300 `
  --output-cpp build/watch_weather_resources.cpp `
  --report build/watch_weather_report.json

生成安装式 .jfapp bundle:

python tools/jellyframe_cli.py package `
  --root samples/apps/packages/watch_weather `
  --target round-300 `
  --output-bundle build/watch_weather.jfapp `
  --report build/watch_weather_report.json

生成给编辑器工具或人工检查使用的 debug 目录:

python tools/jellyframe_cli.py package `
  --root samples/apps/packages/watch_weather `
  --target round-300 `
  --output-cpp build/watch_weather_resources.cpp `
  --report build/watch_weather_report.json `
  --debug-dir build/watch_weather.jfdir

通过 Win32 shell capture path 预览源包:

python tools/jellyframe_cli.py preview `
  --root samples/apps/packages/watch_weather `
  --target round-300 `
  --output build/watch_weather.ppm

packagecheckpreview 和源码包 install 默认先运行 package validation。preview 随后准备与 .jfapp/C++ output 相同的临时 debug package,因此 static module source 会在 Win32 执行 script 前合并;之后再通过临时 render-core 伪浏览器对 package entry HTML 跑一遍管线,让 parser/style/layout/layer diagnostics 来自真正处理 markup 和 CSS 的组件。preview 随后用 Win32 shell capture path 生成实际 app/package 图片。随后会默认运行字体资源预检,使用 16x16 glyph 预算估算;可以用 --font-budget WxH 调整估算,用 --font-coverage 检查嵌入式字体覆盖,用 --emit-used-chars 写出 used chars 文件。只有明确不需要字体检查时才传入 --no-font-check--skip-check 会跳过包括管线和字体在内的开发预检。

CLI 会把 render-core 伪浏览器输出合并进指定 JSON report 的 pipelineDiagnostics 字段。任何 severity 为 error 的诊断都会使 checkpreviewpackage 失败。warning 默认写入报告但不阻断; CI 或发布打包希望 warning 也失败时,传入 --strict

Package report 还包含 fontDiagnostics。这是工具层估算,不是运行时字体加载器:它会扫描 package 文本资源中出现的 codepoints,套用目标 fontProfile,解析 manifest 声明的 .jffont V0 glyph table,并把剩余缺失的非 ASCII glyph 作为 warning 报告。.ttf.otf.woff.bdf 等字体文件可以作为文档或未来工具资源被打包,但当前不是 runtime-loadable font supplement;如果写进 manifest fonts,会被报告为不支持的字体资源格式。

Package report 还包含 imageDiagnostics。这是打包期 target profile 检查,不是核心内置图片 解码器。它会按扩展名分类 package-local 图片资源(bmppngjpegwebpgifunknown),读取便宜的 BMP/PNG metadata,并记录所选 target preset 是否通过 hostServices.imageDecodehostServices.imageCodecs 声明支持。无压缩 24/32-bit BMP 是 当前 Win32 debug shell 已验证的包内图片路径;PNG/JPEG/WebP 或厂商格式必须先由产品 host 接入 codec adapter,preset 才应标为 supported。target 不支持的 codec 和非法 BMP header 会在 install 或 preview 前以 package warning 报告。

backgroundImageDiagnostics 检查受限的 CSS 图片背景路径。它接受一个包内绝对路径 background-image: url("/assets/image.bmp")background: url(...),并为被拒绝的 URL、 缺失的 package 资源或非图片资源输出稳定 warning。它不增加 decoder;目标是否能够使用相应 图片 codec 仍由 target 决定。

Windows 上做人类 app 开发时,应优先使用交互式 Win32 browser 壳:

.\build\desktop-release\Release\jellyframe_desktop_shell.exe --app samples\apps\packages\watch_weather

也可以直接打开 .jfapp,用于验证安装包与源目录渲染是否一致:

.\build\desktop-release\Release\jellyframe_desktop_shell.exe --app build\watch_weather.jfapp
.\build\desktop-release\Release\jellyframe_desktop_shell.exe --capture build\watch_weather.bmp --app build\watch_weather.jfapp

如果需要验收 manifest fonts 中声明的 .jffont 是否真的可被当前包使用,给 Win32 壳加上 --use-app-fonts。默认路径仍使用 GDI 文本,方便桌面调试和截图审查。

.\build\desktop-release\Release\jellyframe_desktop_shell.exe --capture build\watch_weather_font.bmp --app build\watch_weather.jfapp --use-app-fonts

推荐的源码包安装路径是让 CLI 一次完成 validation、pipeline diagnostics、bundle 生成和 registry 安装:

python tools/jellyframe_cli.py install `
  --store build/installed_apps `
  --root samples/apps/packages/watch_weather `
  --target round-300 `
  --report build/watch_weather.install.report.json

源码包安装时,这份 report 仍是普通 package/preflight report;bundle 提交后会额外包含 installTransaction 字段。该字段记录本次操作是 install、update 还是 reinstall,已校验的 bundle 身份、rollback 可用性和数据保留策略。安装已有 bundle 时,tools/app_registry.py install --report 可把同样的 transaction report 写成独立文件。

安装/更新默认拒绝更低 versionCode 的 package。普通用户可见的恢复应使用 rollback; --allow-downgrade 只用于明确的维护或工厂流程,并会写入 installTransaction.updatePolicy

启动器和 system shell 应优先消费派生出的 app-manager state report,而不是直接重复解析原始 registry 字段:

python tools/jellyframe_cli.py registry state `
  --store build/installed_apps `
  --output build/app_manager.state.json

state report schema 是 tools/schemas/jellyframe.app_manager.state.schema.json。 它提供由持久 registry 派生出的 launchablerollbackReady、summary 计数和稳定 failure 详情。 Win32 system-shell UI 也使用同一套派生 state helper 判断可启动状态和 rollback 标记,避免各个 launcher 重复实现 registry 规则。

随后 Win32 壳可以显示 installed-app registry,通过渲染出的 system-shell UI 启动 app,并删除非活动 app:

.\build\desktop-release\Release\jellyframe_desktop_shell.exe --registry-store build/installed_apps
.\build\desktop-release\Release\jellyframe_desktop_shell.exe --registry-store build/installed_apps --launch-app org.jellyframe.examples.weather
.\build\desktop-release\Release\jellyframe_desktop_shell.exe --capture build/app_manager.bmp --registry-store build/installed_apps

低层 registry helper 仍可用于安装已有 .jfapp bundle,或编写 registry 测试脚本:

python tools/jellyframe_cli.py registry install `
  --store build/installed_apps `
  --bundle build/watch_weather.jfapp

python tools/jellyframe_cli.py install `
  --store build/installed_apps `
  --candidate build/watch_weather.install_candidate.json `
  --report build/watch_weather.candidate.install.report.json

python tools/jellyframe_cli.py registry install `
  --store build/installed_apps `
  --bundle build/watch_weather_old.jfapp `
  --allow-downgrade

python tools/jellyframe_cli.py registry list --store build/installed_apps

$bundle = python tools/jellyframe_cli.py registry path `
  --store build/installed_apps `
  --id org.jellyframe.examples.weather

.\build\desktop-release\Release\jellyframe_desktop_shell.exe --app $bundle

python tools/jellyframe_cli.py registry remove `
  --store build/installed_apps `
  --id org.jellyframe.examples.weather

python tools/jellyframe_cli.py registry remove `
  --store build/installed_apps `
  --id org.jellyframe.examples.weather `
  --keep-data

python tools/jellyframe_cli.py registry delete-data `
  --store build/installed_apps `
  --id org.jellyframe.examples.weather

python tools/jellyframe_cli.py registry rollback `
  --store build/installed_apps `
  --id org.jellyframe.examples.weather

python tools/jellyframe_cli.py registry disable `
  --store build/installed_apps `
  --id org.jellyframe.examples.weather

python tools/jellyframe_cli.py registry enable `
  --store build/installed_apps `
  --id org.jellyframe.examples.weather

.\build\desktop-release\Release\jellyframe_desktop_shell.exe `
  --registry-store build/installed_apps `
  --delete-app-data org.jellyframe.examples.weather

registry mock 会先校验 .jfapp header、section range、CRC32 和 manifest summary,再把 bundle 复制到 staging,最后用原子 JSON 写入提交 registry.json。Win32 system-shell 模式使用同一份 registry 格式完成 app discovery、launch、非活动 app deletion、显式数据删除和 V0 回滚验收。 更新 app 时会保留上一版 bundle 作为 rollback 目标,并记录 statusenabledupdatedAtUtc 等产品状态字段;tools/schemas/jellyframe.installed_apps.registry.schema.json 记录这个桌面 schema,并把 rollback-ready 视为派生状态。回滚会交换当前版本和上一版 bundle 元数据,不触碰 app 私有数据。 禁用 app 会保留 bundle 和数据,但拒绝启动,直到重新启用。启动/加载失败可以把 entry 标记为 failed 并记录 failure reason;显式 enable 会清除该 failure 记录。 桌面 registry store 中的 app 私有数据位于 data/<sanitized-app-id>。删除 app 默认删除这份数据; --keep-data 会保留数据,delete-data / --delete-app-data 可在不移除已安装 bundle 的情况下 单独删除数据。

render-core 伪浏览器继续作为独立 HTML/CSS 页面和 package entry 预检的确定性 CI 壳。 App/package 截图与人工调试应走 Win32 shell,这样同一路径能覆盖 package loading、scripting、 input 和 host-shell 行为。

对 package 文件运行校验、管线诊断和默认字体资源检查:

python tools/jellyframe_cli.py check `
  --root samples/apps/packages/watch_weather `
  --target round-300 `
  --report build/watch_weather_report.json

收集 package 使用到的字符,并可选择生成嵌入式 bitmap font header:

python tools/jellyframe_cli.py font `
  --root samples/apps/packages/watch_weather `
  --target round-300 `
  --report build/watch_weather_report.json `
  --used-chars build/watch_weather_used_chars.txt `
  --font-budget 16x16

如果已有 BDF 源字体,追加 --bdf--output-header 生成固件静态 header:

python tools/jellyframe_cli.py font `
  --root samples/apps/packages/watch_weather `
  --target round-300 `
  --report build/watch_weather_report.json `
  --used-chars build/watch_weather_used_chars.txt `
  --font-budget 16x16 `
  --bdf path/to/ui.bdf `
  --output-header build/watch_weather_font.h `
  --name watch_weather_font

也可以生成 .jffont,作为 .jfapp 动态字体补充资源的二进制输入:

python tools/jellyframe_cli.py font `
  --root samples/apps/packages/watch_weather `
  --target round-300 `
  --report build/watch_weather_report.json `
  --used-chars build/watch_weather_used_chars.txt `
  --bdf path/to/ui.bdf `
  --output-binary build/watch_weather_font.jffont

package report 会在 serviceIntent 中记录 manifest service intent。这个稳定摘要包含 network.fetchstorage.kvmedia.audio.playback 请求、backgroundServices 声明,以及提醒 工具和作者“最终授权仍由 host profile/product policy 决定”的 policy notes。如果 target preset 声明了 可选 hostServices,report 还会用 targetSupportsupportedunsupportedunknown 报告 network fetch、app 私有 KV、audio playback、语义传感器和定位的目标支持状态。如果 app 请求了 被所选 target 明确标为 unsupported 的服务,package 会输出 service-target-unsupported warning;unknown 只作为 信息保留,因为很多产品 port 会在通用 preset 之外决定可选服务。桌面 app-runtime mock 可以验证 request/completion/handle 契约,app_service_policies_for_app(...) 会把这些请求与 host/profile 策略合成。核心不会执行真实网络或文件系统 I/O。

Package report 还包含 scriptApiDiagnostics。这是打包期静态检查,只扫描 package-local classic script、打包期静态 module source 和 HTML inline script,不执行 JavaScript。当前会识别 XMLHttpRequestlocalStorageAudio()navigator.geolocation.getCurrentPosition(...)getContext("2d")。如果脚本使用这些宿主支持 API,但 manifest 没有声明对应 capability, 工具会输出 script-capability-missing warning;CLI 的 developerAdvice[] 会把它解释为 “补 manifest capability,并确认 target profile 支持”。这项检查不进入 MCU runtime,也不替代 真实 host 授权。无参 Date() / new Date() 会输出 script-host-time-ambiguous; 需要宿主时钟时使用 Date.now(),需要 Date 对象时使用 new Date(Date.now())fetch()PromiseinnerHTML、动态 import()WebSocketEventSourceBroadcastChannelDataTransfer、Web Workers 和 serviceWorker 等延后 API 会输出 script-api-deferred;复杂 querySelector / querySelectorAll 字面量会输出 script-api-subset,因为运行时只支持简单 selector 子集。JSON summary 中的 missingCapabilityCount 只统计缺失 manifest capability 的项目,warningCount 统计所有脚本预检 warning,包括延后或子集 API。

Package report 还包含 htmlApiDiagnostics,这是对 package-local HTML 文件的轻量静态扫描。 它会提示容易被误认为“浏览器能力已支持”的平台语义标签或行为,例如 iframeembedobject、Shadow DOM slot、image map,以及 form action/method 提交。Form V0 已支持本地 requestSubmit()、约束检查、可取消 submit 事件与 FormData;该 warning 只针对浏览器导航和 自动 HTTP 提交。未知自定义标签仍会作为普通元素保留;这项预检只针对 JellyFrame 明确不实现的强 浏览器语义能力。

JSON report 面向 CI 和编辑器集成,包含 app 元信息、选中的 target config、effective budgets、 资源大小、CRC32/SHA-256 校验、service intent、runtimeBudgetEstimate、local/remote reference 诊断、htmlApiDiagnosticsscriptApiDiagnosticsimageDiagnosticsfontDiagnostics、package-resource warnings 和 pipelineDiagnostics。CLI 还会从同一份 package 和 pipeline 数据派生 performanceSummary 和可选 performanceAdvice[],量化静态预检阶段可判断的性能风险:对象数量、layer/display command 数量、framebuffer bytes、估算 pipeline heap、资源预算占比和 full-frame present 规模。存在伪浏览器 diagnostics 时,summary 也会携带桌面工具侧阶段耗时微秒数 (timingsUs),用于判断 parse、layout、paint 或 present 哪一步占主导;这些耗时是归因数据, 不是设备 FPS。runtimeBudgetEstimate 是 package-preflight 估算:它报告打包阶段已知的 resource/font 用量和 manifest/target budget 上限;真实运行时的 queue/handle/timer/listener 计数来自 host/runtime capture 路径中的 AppBudgetSnapshot。管线诊断包含伪浏览器格式/版本标记、输出 viewport、面向内存的管线统计、 severity 汇总,以及 parser、style、layout、layer、renderer 代码实际发出的 diagnostics。 已知不支持或降级的特性应给出明确原因;未知恢复至少应包含触发字段或片段。 如果你保存了 Win32 frame-script 或 capture 日志,可以通过 --runtime-log 交给 CLI;它会把 frame-update、present-estimate 和 load-telemetry 计数合并进同一份 report,写入 runtimeMetrics 和测得的 performanceSummary 字段。 如果你有真实设备或 port 层日志,可以通过 --port-telemetry 合并到同一份 report。该文件可以是 JSON,也可以包含一行简单文本:

port_telemetry frames=60 full=1 dirty=59 flushes=90 frame_ms_avg=24.5 frame_ms_max=38.0 dma_wait_ms_avg=2.1 flush_done_ms_avg=7.4 internal_ram_peak=180000

CLI 会把它写入 portTelemetry,并在 performanceSummary 中补充 measuredPortAverageFrameMsmeasuredPortMaxFrameMsmeasuredPortAverageDmaWaitMsmeasuredPortAverageFlushDoneMsmeasuredPortInternalRamPeakBytes 等字段。超过 30 FPS 预算、DMA/flush 时间或 internal RAM 压力明显偏高时,performanceAdvice[] 会给出面向 app 作者和 port 作者的分层建议。这仍是桌面工具输入,不是 MCU runtime 需要解析的格式。

当 report 由 tools/jellyframe_cli.py 写出时,CLI 还会从 package warnings、管线 diagnostics、responsive profiles 和字体 diagnostics 派生 developerAdvice[]。这个字段面向 app 作者和编辑器集成:每条保留稳定诊断 code、severity,以及可选 source/target/detail, 再补一段简短解释和优先修复动作。它不替代底层 diagnostics,也不进入 MCU runtime。未来新增但还没 专门写 advice 文案的诊断码仍会得到通用 review 项,避免 app 作者只看到沉默的失败。

原始 warning 能识别 API、HTML tag、capability、selector 或 layout 元素时,advice 会保留为 apitagcapabilityselectorpath,并在 diagnosticContext 中重复最佳可用 subject。 编辑器集成应使用这些字段做精确定位;action 是面向人的首个修复建议,不是供解析依赖的契约。

安装/升级/删除生命周期:

  • 打包和 registry install 负责校验、staging 与提交字节;用户可见的 app 生命周期决策由 system shell 持有。
  • update 默认保留 app 私有数据,并在切换 bundle 前 flush pending writes。
  • uninstall 默认 drop pending writes 并删除 app 私有数据。产品 app manager 可以通过调整 AppStorageLifecyclePolicy 提供明确的“保留数据”选项。
  • crash/watchdog/memory-pressure recovery 应优先 drop pending writes 并回到 launcher/system shell, 不应阻塞等待 flash 写入。
  • 宿主在模拟这些操作时,应把 apply_app_storage_lifecycle(...) 产生的稳定 storage lifecycle diagnostics 输出到开发日志或验证报告中。

tools/package_app.py 仍作为 CLI 和嵌入式构建集成使用的底层 packer。

内置 weather、clock、timer 和 calculator 模板都是普通 source package。它们覆盖 event delegation、dataset、timers、grid layout 和 form controls,不会在 HTML/CSS/JS 之上再引入一层 app framework。

可选 VS Code 辅助扩展位于 tools/vscode-jellyframe。它提供 manifest schema 关联, 并通过命令面板调用 tools/jellyframe_cli.py 完成 validate、check、preview、package 和模板创建,并提供启动所选 package 的 Win32 交互浏览器命令。它会读取 CLI report, 为 package warnings 和 pipelineDiagnostics 显示报告面板与 inline diagnostics。 CLI 仍是 CI 和非 VS Code 工作流的权威入口。

参考资料: