Flipper Zero AppManifests 完全指南:application.fam 清单格式与 fbt 构建系统深度解析
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
Flipper Zero 固件中的每一个组件——系统服务、主菜单应用、设置项、调试工具乃至 SD 卡上的外部应用——都通过一个名为application.fam的清单文件在构建系统中声明其身份与依赖。本文以 documentation/AppManifests.md 为骨架,结合仓库内application.fam实例与 fbt 构建工具、脚本/fbt/appmanifest.py 的源码实现,完整讲解 FAM 清单的全部参数、外部应用(FAP)专属字段、依赖与冲突处理机制,帮助你写出可被fbt正确识别、编译与打包的应用清单。
FAM 在构建系统中的定位
Flipper Zero 固件的所有组件——服务、用户应用与系统设置——均独立开发,每个组件对应一份名为application.fam的清单文件,用于定义该组件的基本属性及其与系统其他部分的关系。
构建固件时,fbt会收集所有应用清单并处理其依赖关系,随后只构建当前构建配置中实际引用的组件(详见 FBT 文档)。因此,application.fam是fbt理解"有哪些应用、各自需要什么"的唯一入口——没有清单的应用不会被构建,清单中声明的依赖决定了编译时的可用符号与源码集合。
从源码结构看,清单的实际处理逻辑集中在 scripts/fbt/appmanifest.py:FlipperAppType枚举定义了全部应用类型,FlipperApplication数据类声明了所有可用的字段与默认值,AppManager负责加载并校验清单(重复的appid会直接抛FlipperManifestException),AppBuildset则完成依赖解析、冲突检查与目标平台匹配。
App 定义与必填参数
一个固件组件的属性通过一段 Python 代码片段声明,即调用带各种参数的App()函数。清单文件可包含一个或多个App()定义,例如 applications/services/bt/application.fam 同时定义了蓝牙服务与蓝牙设置两个组件。
只有两个参数是必填的:
- appid:字符串,构建系统内的应用 ID,用于指定构建配置中包含哪个应用,以及解析依赖与冲突。
fbt强制要求其匹配正则^[a-z0-9_]+$(见 scripts/fbt/appmanifest.py#L35),即只能使用小写字母、数字与下划线,且不允许重复声明。 - apptype:
FlipperAppType.*枚举的成员,决定组件的固件角色,详见下表:
| 枚举成员 | 固件组件类型 |
|---|---|
| SERVICE | 系统服务,在系统启动早期创建 |
| SYSTEM | 不出现在任何菜单中的应用,可由其他应用或 CLI 启动 |
| APP | 主菜单中的常规应用 |
| PLUGIN | 作为固件一部分构建、放置于插件菜单中的应用 |
| DEBUG | 仅在开启调试模式时于调试菜单中可见的应用 |
| ARCHIVE | 唯一且仅有的 Archive 应用 |
| SETTINGS | 放置在系统设置菜单中的应用 |
| STARTUP | 系统启动时运行的回调函数,不定义独立应用 |
| EXTERNAL | 构建为.fap插件的外部应用 |
| METAPACKAGE | 不定义任何要运行的代码,用于声明依赖与应用捆绑包 |
值得注意的是,scripts/fbt/appmanifest.py#L19-L30 中还存在一个文档表格未列出的成员MENUEXTERNAL,它属于外部应用类型,但按 AppBuildset 定义 仅在显式列入应用集合时才构建(always deploy为False)。
通用参数详解
除appid与apptype外,其余参数均为可选,且只对特定应用类型有意义:
- name:菜单中显示的名称。例如蓝牙设置应用在菜单中显示为 "Bluetooth"(见 applications/services/bt/application.fam#L127)。
- entry_point:用作应用入口的 C 函数名。C++ 函数名会被编译器改编(mangling),如需作为入口点必须用
extern "C"包裹。 - flags:系统应用的内部标志,普通开发不应使用。
- cdefines:当当前应用被包含进激活的构建配置时,为其他应用全局声明的 C 预处理器定义;对外部应用:这些定义仅用于构建该应用自身。典型例子如蓝牙服务声明
cdefines=["SRV_BT"](见 applications/services/bt/application.fam#L6)。 - requires:应用 ID 列表,当当前应用被列入待构建应用清单时,这些应用也会被加入构建配置。
- conflicts:应用 ID 列表,若构建出的应用列表中出现任一冲突应用,
fbt将中止固件构建。 - provides:功能上与
requires完全一致,常用于将若干应用打包声明为"提供"集合(见下文 METAPACKAGE 示例)。 - stack_size:应用启动时分配的栈大小(字节)。栈分配过小会导致栈溢出引发系统崩溃,过大则会减少应用可用的堆内存。可用
top和freeCLI 命令分析应用内存占用。源码默认值为2048字节(见 scripts/fbt/appmanifest.py#L62)。 - icon:作为固件一部分构建时,从内置资源中选取的动画图标名。
- order:应用在其所属分组内的排序值,值越小越靠前,用于排序启动钩子与菜单项。蓝牙服务与蓝牙设置的
order分别为 20 与 40(见 applications/services/bt/application.fam)。 - sdk_headers:该应用代码中需要包含进外部应用 API 定义的 C 头文件列表。蓝牙服务声明了
bt_service/bt.h与bt_service/bt_keys_storage.h(见 applications/services/bt/application.fam#L17)。 - targets:应用兼容的目标平台名列表,未指定时默认为
["all"],即所有目标。仓库中硬件相关的调试工具常显式限定目标,例如 applications/debug/accessor/application.fam#L5 声明targets=["f7"]。判断逻辑见 scripts/fbt/appmanifest.py#L97-L98:当目标名在targets中或targets含"all"时视为支持。 - resources:应用源码文件夹内用于打包 SD 卡资源的子文件夹名,仅当应用被包含进构建配置时才生效,默认值
""表示不打包资源。
METAPACKAGE:依赖声明与应用捆绑包
METAPACKAGE类型不产生任何可运行代码,专门用于声明依赖集合,是组织固件应用清单的常用手段。例如 applications/main/application.fam 中的main_apps元包通过provides一次性声明了主菜单全部核心应用(gpio、ibutton、infrared、lfrfid、nfc、subghz、bad_usb、u2f、archive 等);applications/debug/application.fam 中的debug_apps元包则把 blink_test、vibro_test、keypad_test 等全部调试应用捆绑在一起。
这些元包 ID 正是 fbt_options.py 中 FIRMWARE_APPS 引用的集合名(如"main_apps"、"system_apps"、"settings_apps")。由于requires与provides都会触发依赖解析(见 scripts/fbt/appmanifest.py#L303-L334 的_process_deps循环),引用一个元包即可递归带入其提供的全部应用。
PLUGIN 与外部应用的构建约束
fbt对 PLUGIN 与普通内建应用有不同的参数约束,校验逻辑位于 scripts/fbt/appmanifest.py#L144-L173:
- PLUGIN 必须声明
requires:插件必须通过requires指明宿主应用(父应用),缺少该字段会直接报错。 - PLUGIN 不能设置
stack_size:插件共享宿主应用的进程上下文,stack_size会被强制置 0(见 scripts/fbt/appmanifest.py#L108-L110),若显式传入 stack 值会提示"是否本意是 EXTERNAL 类型"。 fal_embedded仅适用于 PLUGIN:普通应用设置该字段会被拒绝。- 外部分发类应用(EXTERNAL、PLUGIN、DEBUG)不能使用
resources字段(资源打包另有fap_file_assets机制);内建应用则不能使用fap_extbuild、fap_private_libs、fap_icon_assets等 FAP 专属字段。
实际示例可参考 applications/examples/example_plugins/application.fam:example_plugins与example_plugins_multi是 EXTERNAL 宿主应用,example_plugin1与example_plugin2是 PLUGIN,分别通过requires=["example_plugins", "example_plugins_multi"]与requires=["example_plugins_multi"]挂载到宿主上,并通过sources=["plugin1.c"]、sources=["plugin2.c"]指定各自的源码文件。插件归属关系由AppBuildset._group_plugins处理(见 scripts/fbt/appmanifest.py#L397-L423)。
外部应用(FAP)专属参数
以下参数仅用于构建 FAP(Flipper App Package,即.fap文件,可独立于固件版本运行,详见 AppsOnSDCard.md):
- sources:字符串列表,用于在应用文件夹内收集源文件的文件名掩码,默认值为
["*.c*"](同时包含 C 与 C++ 源码)。应用不能使用"lib"文件夹存放自身源码,因为它被保留给fap_private_libs。以"!"开头的路径从源文件列表中排除,掩码可含通配符与目录名。例如["*.c*", "!plugins"]会收集应用文件夹内除plugins(及lib)文件夹外的全部 C/C++ 源码;不含通配符(*、?)的路径则按完整字面路径处理。 - fap_version:字符串,应用版本,默认
"0.1",也可使用(x, y)形式的二元元组。版本可追加更多点分隔部分(如补丁号),但只有主版本号与次版本号会被写入构建出的.fap。源码中会将其解析为整数元组并校验至少两个分量(见 scripts/fbt/appmanifest.py#L115-L123)。 - fap_icon:
.png文件名,要求 1 位色深、10x10 像素,嵌入.fap文件内部。 - fap_libs:额外链接库列表,可访问未作为主固件 API 导出的额外函数,代价是
.fap文件体积与 RAM 消耗增加。 - fap_category:字符串,可为空,应用子分类,同时决定 FAP 在文件系统 apps 文件夹中的存放路径。
- fap_description:字符串,可为空,应用简介。
- fap_author:字符串,可为空,应用作者。
- fap_weburl:字符串,可为空,应用主页。
- fap_icon_assets:字符串,定义收集应用图片资源的文件夹名,这些图片会被预处理并随应用一起构建,使用方式见 AppsOnSDCard.md 的 FAP assets 章节。
- fap_extbuild:支持应用的部分源码由外部工具构建,包含一组
ExtFile(path="文件名", command="shell 命令")定义,fbt会为列表中的每个文件执行对应命令。 - fal_embedded:布尔值,默认
False,仅适用于 PLUGIN 类型。若为True,插件会作为资源嵌入宿主应用的.fap文件,宿主启动时解压到apps_assets/APPID文件夹,从而随宿主应用一并分发。
fap_extbuild 与 Rust 构建示例
外部构建命令在固件根目录执行,所有中间文件必须放在应用的临时构建文件夹中。为此可借助fbt的模式展开:${FAP_WORK_DIR}替换为应用临时构建文件夹路径,${FAP_SRC_DIR}替换为应用源码文件夹路径,也可使用fbt内部定义的其他变量。
以下示例展示了如何从 Rust 源码构建应用(引自 documentation/AppManifests.md#L64-L74):
sources=["target/thumbv7em-none-eabihf/release/libhello_rust.a"], fap_extbuild=( ExtFile( path="${FAP_WORK_DIR}/target/thumbv7em-none-eabihf/release/libhello_rust.a", command="cargo build --release --verbose --target thumbv7em-none-eabihf --target-dir ${FAP_WORK_DIR}/target --manifest-path ${FAP_SRC_DIR}/Cargo.toml", ), ),即先用cargo在临时目录中交叉编译出静态库,再将其作为应用源码链接进.fap。
fap_private_libs 与私有库示例
fap_private_libs:随应用以源码形式分发的额外库列表,这些库会作为应用构建过程的一部分被编译。库源码必须放在应用源码文件夹内的lib子文件夹中。每个库通过调用Lib()函数定义,参数如下:
- name:库文件夹名称。必填。
- fap_include_paths:加入父应用 include 路径列表的库相对路径,默认
["."],即库源码根目录。 - sources:收集库源码的文件名掩码列表,路径相对库源码根目录,默认
["*.c*"]。 - cflags:构建该库时附加的编译器标志列表,默认
[]。 - cdefines:构建该库时附加的预处理器定义列表,默认
[]。 - cincludes:构建该库时附加的 include 路径列表,路径相对应用根目录,可用于为库代码提供外部搜索路径(如配置头文件),默认
[]。
以下示例(引自 documentation/AppManifests.md#L89-L106)同时声明了两个私有库:
fap_private_libs=[ Lib( name="mbedtls", fap_include_paths=["include"], sources=[ "library/des.c", "library/sha1.c", "library/platform_util.c", ], cdefines=["MBEDTLS_ERROR_C"], ), Lib( name="loclass", cflags=["-Wno-error"], ), ],对于该片段,fbt将构建两个库:一个来自lib/mbedtls文件夹的源码,另一个来自lib/loclass文件夹。对mbedtls库,fbt会把lib/mbedtls/include加入应用 include 路径,只编译sources列表指定的文件,并为其源码启用MBEDTLS_ERROR_C预处理器定义;对loclass库,fbt会把lib/loclass加入应用 include 路径并构建该文件夹内全部源码,同时禁用"将编译警告视为错误",这在编译大型第三方代码库时非常实用。两个库最终都会与应用链接。
.fam 文件内容与完整实例
.fam文件包含一个或多个应用定义。以下为 applications/services/bt/application.fam 的完整内容,它同时展示了 STARTUP 与 SETTINGS 两种应用类型(文档示例中的order=70在仓库当前版本中为 40,以仓库实际文件为准):
App( appid="bt", name="BtSrv", apptype=FlipperAppType.SERVICE, entry_point="bt_srv", cdefines=["SRV_BT"], requires=[ "cli", "dialogs", ], provides=[ "bt_start", "bt_settings", ], stack_size=1 * 1024, order=20, sdk_headers=["bt_service/bt.h", "bt_service/bt_keys_storage.h"], ) App( appid="bt_start", apptype=FlipperAppType.STARTUP, entry_point="bt_on_system_start", order=40, )这个实例覆盖了常见参数的典型用法:requires声明运行依赖(CLI 与对话框服务)、provides提供对子组件(启动钩子、设置应用)的引用、sdk_headers导出供外部应用使用的头文件。仓库内其余.fam文件(如 applications/examples/example_plugins/application.fam、applications/examples/example_adc/application.fam)可作为编写清单的更多参考。
fbt 对清单的处理流程
结合 scripts/fbt/appmanifest.py 的源码,fbt对清单的处理可归纳为以下流程:
- 加载与语法检查:
AppManager.load_manifest读取application.fam,以exec方式执行其 Python 代码,App()、ExtFile()、Lib()三个函数在此上下文注册。清单语法错误或未产生任何App()定义都会报错(见 scripts/fbt/appmanifest.py#L175-L214)。 - 参数校验:
_validate_app_params按应用类型检查字段合法性(PLUGIN 必须requires、禁止stack_size;内建应用禁止 FAP 专属字段等),__post_init__校验appid正则与fap_version格式。 - 依赖解析:
AppBuildset._process_deps反复遍历应用集合,将requires与provides指向的应用迭代加入集合,直至不再新增(见 scripts/fbt/appmanifest.py#L325-L334)。 - 外部应用归类:
_process_ext_apps按EXTERNAL_APP_TYPES_MAP收集外部应用,并根据硬件目标兼容性分成可构建与不兼容两组。 - 冲突检查:
_check_conflicts检查conflicts字段,发现冲突即以App conflicts for ...中止构建;_check_unsatisfied检查requires中缺失的应用。 - 目标平台匹配:
_check_target_match确保选中应用均支持当前构建目标,supports_hardware_target依据targets字段判断。 - 插件分组:
_group_plugins将 PLUGIN 挂载到其requires指定的宿主应用名下,供后续嵌入或菜单组织使用。
构建配置层面,fbt_options.py 中的FIRMWARE_APPS定义了"default"与"unit_tests"两套应用集合(分别引用basic_services、main_apps、system_apps、settings_apps等元包 ID),FIRMWARE_APP_SET选择实际使用的集合;命令行可通过--extra-int-apps、--extra-ext-apps强制追加内建或外部应用(详见 fbt.md)。
常见错误与排查建议
Invalid appid:appid包含大写字母、连字符等非法字符,未匹配^[a-z0-9_]+$。统一使用小写蛇形命名。Duplicate app declaration:两个清单声明了相同appid,全局唯一。Plugin ... cannot have stack:PLUGIN 类型误设stack_size,若需要独立栈应改用 EXTERNAL 类型。Plugin ... must have 'requires':PLUGIN 未声明宿主应用。App conflicts for ...:构建集合中出现conflicts冲突组合,需调整应用集合或移除冲突声明。Unsatisfied dependencies for ...:requires引用的appid不存在,检查拼写或确认该应用是否已声明清单。App manifest ... is malformed:清单文件语法错误或未包含任何App()调用。Skipping ... due to target mismatch:应用targets不含当前构建目标,属预期跳过行为而非错误。
这些校验信息大多直接来自 scripts/fbt/appmanifest.py 中的异常与提示文本,遇到构建失败时可按提示逐项核对清单字段。
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考