Flipper Zero 固件应用清单(FAM)完全指南:从 application.fam 到构建系统
【免费下载链接】unleashed-firmwareFlipper Zero Unleashed Firmware项目地址: https://gitcode.com/GitHub_Trending/un/unleashed-firmware
FAM(Flipper App Manifest,Flipper 应用清单)是 Unleashed 固件中每个组件——系统服务、用户应用、系统设置乃至插件——在构建系统内的"身份证"。本文以 documentation/AppManifests.md 为骨架,结合仓库中真实.fam文件与构建工具 scripts/fbt/appmanifest.py 的实现细节,系统讲解App()的全部参数、FAP 外部应用专属配置、私有库与外部构建扩展,帮助你从零写出一份可被fbt正确解析、参与固件编译的应用清单。
FAM 是什么:每个组件的构建契约
Unleashed 固件的所有组成部分——服务(services)、用户应用(user applications)、系统设置(system settings)——都是独立开发的。每个组件目录下都有一份名为application.fam的清单文件,用 Python 语法声明该组件的基本属性及其与系统其他部分的关系。
当构建固件时,fbt(Flipper Build Tool)会:
- 收集所有应用清单(manifest);
- 处理它们之间的依赖关系(
requires/provides/conflicts); - 只构建当前构建配置中引用的那些组件。
关于构建配置(如COMPACT、DEBUG、FIRMWARE_APP_SET等选项)的细节,参见 FBT 文档。清单解析的实际入口在 scripts/fbt/appmanifest.py:fbt读取.fam文件内容并exec执行,其中预置了App()、ExtFile()、Lib()三个可调用对象;如果解析失败或文件内没有任何App()定义,会抛出FlipperManifestException。
App 定义与 App() 函数
一个固件组件的属性通过一段 Python 代码片段声明,即调用App()函数并传入各种参数。只有两个参数是强制性的:appid和apptype,其余均为可选,且可能只对特定 app 类型有意义。
App()在源码中对应FlipperApplication数据类(见 scripts/fbt/appmanifest.py),其中还隐藏着一些"默认值纪律":例如PLUGIN类型的应用会被强制将stack_size置为 0(因为它不独立运行线程),而appid必须匹配正则^[a-z0-9_]+$,否则直接报错。
appid:应用唯一标识
字符串类型,构建系统内的应用 ID。它用于:
- 在构建配置中指定要包含哪个应用;
- 解析依赖(
requires/provides); - 检测冲突(
conflicts)。
注意命名规范:appid只能包含小写字母、数字和下划线,且不能以数字开头(由 appmanifest.py 中的APP_ID_REGEX强制校验),重复声明同一appid也会被拒绝(见 appmanifest.py)。
apptype:组件类型
apptype是FlipperAppType.*枚举的一个成员,枚举定义在 scripts/fbt/appmanifest.py。各取值含义如下:
| 枚举成员 | 固件组件类型 |
|---|---|
| SERVICE | 系统服务,在系统启动早期创建 |
| SYSTEM | 不显示在任何菜单中的应用,可由其他应用或 CLI 启动 |
| APP | 主菜单中的常规应用 |
| PLUGIN | 作为固件一部分构建、放入 Plugins 菜单的应用 |
| DEBUG | 仅在启用调试模式后于 Debug 菜单中显示的应用 |
| ARCHIVE | 唯一且仅有的 Archive 应用 |
| SETTINGS | 放入系统设置菜单的应用 |
| STARTUP | 系统启动时运行的回调函数,不定义独立应用 |
| EXTERNAL | 构建为.fap插件的外部应用 |
| METAPACKAGE | 不定义任何可运行代码,仅用于声明依赖和应用捆绑包 |
从源码看,枚举中实际还包含MENUEXTERNAL(作为主菜单外部应用构建)和EXTSETTINGS(作为设置菜单外部应用构建)两个成员,它们被归类到外部应用类型映射中(见 appmanifest.py)。
apptype还决定了清单参数校验规则(见 appmanifest.py):
PLUGIN不能设置stack_size(提示"did you mean FlipperAppType.EXTERNAL?"),且必须声明requires;- 非
PLUGIN类型不能设置fal_embedded; - 外部分发类型(
EXTERNAL、PLUGIN、DEBUG)不能设置resources参数; - 内置类型不能使用
fap_extbuild、fap_private_libs等 FAP 专属参数。
通用参数详解
以下参数适用于所有 app 类型:
- name:菜单中显示的名称(字符串,可空)。
- entry_point:作为应用入口点的 C 函数名。注意 C++ 函数名会被 name mangling 处理,因此要作为入口点必须用
extern "C"包裹。类型为STARTUP时对应启动回调。 - flags:系统应用的内部标志,不要使用。
- cdefines:当当前应用被包含进构建配置时,全局声明给其他应用的 C 预处理器定义列表。对外部应用:这些定义在构建该应用自身时使用。例如 applications/services/bt/application.fam 中
cdefines=["SRV_BT"]。 - requires:应用 ID 列表。当当前应用被引用时,这些应用也会被加入构建配置(依赖)。
- conflicts:与当前应用冲突的应用 ID 列表。如果其中任何一个出现在构建的应用列表中,
fbt会中止固件构建(见 appmanifest.py 的_check_conflicts)。 - provides:功能上与requires字段完全相同的字段。二者均会被纳入依赖解析(见 appmanifest.py)。
- stack_size:应用启动时分配的栈大小(字节)。栈分配过小会导致系统因栈溢出崩溃,过大则会减少应用处理数据可用的堆内存。可用
top和freeCLI 命令分析应用内存占用。源码中默认值为 2048 字节(appmanifest.py),仓库常见写法如4 * 1024。 - icon:内置资源中的动画图标名,用于将应用构建为固件一部分时显示。例如 applications/main/nfc/application.fam 中的
icon="A_NFC_14"。 - order:应用在所在分组内的排序位置,值越小越靠近列表开头。用于启动钩子(STARTUP)和菜单项的排序。从源码看,
get_apps_of_type会按order对同类型应用排序(appmanifest.py)。 - sdk_headers:本应用代码中要包含进外部应用 API 定义的 C 头文件列表。例如
bt服务声明了"bt_service/bt.h"、"bt_settings.h"等。这些头文件会被收集进 SDK 定义(appmanifest.py)。 - targets:该应用兼容的字符串和目标名列表。不指定则对所有目标构建,默认值为
["all"]。例如accessor应用声明targets=["f7"](applications/debug/accessor/application.fam)。构建时若目标不匹配,fbt会跳过该应用并打印提示(appmanifest.py)。 - resources:应用源文件夹内用于打包 SD 卡资源的子文件夹名。仅当应用被包含进构建配置时才会打包。默认值为
""(不打包)。例如 applications/main/nfc/application.fam 中的resources="resources"。
外部应用(FAP)专属参数
以下参数仅用于 FAP(Flipper Application Package),即构建为.fap外部插件时:
- sources:字符串列表,用于在应用文件夹内收集源码的文件名掩码。默认值为
["*.c*"],即包含 C 和 C++ 源文件。应用不能使用lib文件夹存放自己的源码,该文件夹保留给fap_private_libs。以"!"开头的路径会被从源码列表中排除,同时支持通配符和目录名。例如["*.c*", "!plugins"]会包含应用文件夹内所有 C/C++ 源文件,但排除plugins(和lib)文件夹。不含通配符(*、?)的路径按完整字面路径处理(包含与排除均如此)。 - fap_version:字符串,应用版本。默认值
"0.1"。也可用(x, y)二元组指定版本,还可以追加更多点分部分(如补丁号),但构建出的.fap只存储主版本和次版本号。源码中fap_version会被解析为整数元组(appmanifest.py)。 - fap_icon:
.png文件名,要求 1-bit 色深、10x10 像素,嵌入.fap文件内。 - fap_libs:额外链接的库列表,可访问主固件未导出为 API 的额外函数,代价是增加
.fap文件体积和 RAM 占用。仓库实例:fap_libs=["assets", "mbedtls"](NFC 应用)、fap_libs=["assets"](JS 应用)。 - fap_category:字符串,可空。应用子类别,同时决定 FAP 在文件系统 apps 文件夹中的路径。仓库实例:
fap_category="NFC"、fap_category="assets"、fap_category="Debug"。 - fap_description:字符串,可空。简短应用描述。
- fap_author:字符串,可空。应用作者。
- fap_weburl:字符串,可空。应用主页。
- fap_icon_assets:字符串。若存在,定义用于收集该应用图片资源的文件夹名,这些图片会被预处理并随应用一同构建。详见 FAP assets。
- fap_extbuild:支持应用源码的某部分由外部工具构建。包含
ExtFile(path="file name", command="shell command")定义列表,fbt会为列表中的每个文件运行指定命令。 - fal_embedded:布尔值,默认
False。仅适用于PLUGIN类型。若为True,插件会作为资源嵌入宿主应用的.fap文件中,并在其启动时解压到apps_assets/APPID文件夹,从而允许插件随宿主应用一起分发。仓库中 NFC 的各协议插件(如nfc_iso14443_3a)均设置fal_embedded=True(见 applications/main/nfc/application.fam)。
fap_exclude_libs:排除链接的工具链库
源码中还有一个文档表格之外的 FAP 参数值得一提:fap_exclude_libs。它用于声明"不要静态链接进本应用的库",这些符号保持未定义,改在加载时从固件 API 表解析。例如fap_exclude_libs=["gcc"]可以避免每个.fal都携带一份双精度软浮点辅助函数副本(见 appmanifest.py 的注释),仓库中绝大多数 FAP 都使用了该参数。
fap_extbuild:用外部工具链构建部分源码
fap_extbuild中所有命令都在固件根目录执行,所有中间文件必须放入应用的临时构建文件夹。为此fbt提供模式展开:${FAP_WORK_DIR}替换为应用临时构建文件夹路径,${FAP_SRC_DIR}替换为应用源文件夹路径,也可以使用fbt内部定义的其他变量。
用 Rust 源码构建应用的示例:
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", ), ),这里ExtFile.path指向外部工具(cargo)产出的目标文件,command是生成它的 shell 命令;fbt会先执行命令,再将该文件纳入应用链接。
fap_private_libs:随应用源码分发的私有库
fap_private_libs是随应用一起以源码形式分发的额外库列表,这些库会作为应用构建流程的一部分被编译。库源码必须放在应用源文件夹的lib子文件夹中。每个库通过调用Lib()函数定义,参数如下:
- name:库文件夹名。必填。
- fap_include_paths:要加入父 fap 包含路径列表的库相对路径列表。默认值
["."],即库源码根目录。 - sources:用于收集该库包含文件的文件名掩码列表。路径相对库源码根目录。默认值
["*.c*"]。 - cflags:构建该库时使用的额外编译器标志列表。默认值
[]。 - cdefines:构建该库时使用的额外预处理器定义列表。默认值
[]。 - cincludes:构建该库时使用的额外包含路径列表。路径相对应用根目录,可用于为库代码提供外部搜索路径(如配置头文件)。默认值
[]。
Lib()与ExtFile()一样是清单执行环境的预置对象,对应FlipperApplication.Library数据类(appmanifest.py)。
带私有库的应用构建示例:
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会构建 2 个库:一个来自lib/mbedtls文件夹的源码,另一个来自lib/loclass文件夹的源码。
- 对
mbedtls:fbt会把lib/mbedtls/include加入应用的包含路径列表,只编译sources列表中指定的文件,并为mbedtls源码启用MBEDTLS_ERROR_C预处理器定义; - 对
loclass:fbt会把lib/loclass加入应用包含路径,并构建该文件夹下所有源码。同时通过cflags=["-Wno-error"]禁用将警告视为错误,这在编译大型第三方代码库时非常有用。
两个库最终都会与应用链接在一起。
依赖解析与冲突检测:fbt 的内部工作流
理解清单参数如何参与构建,有助于写出正确的清单。在 scripts/fbt/appmanifest.py 中,AppBuildset的构造过程依次执行:
_process_deps()(appmanifest.py):迭代地把所有已选应用的provides + requires中缺失的依赖(且目标兼容)加入构建集,直到没有新增依赖为止;_process_ext_apps():根据EXTERNAL_APP_TYPES_MAP收集外部应用,并按硬件目标兼容性分流;_check_conflicts():若两个互斥应用同时出现在应用列表中,抛出App conflicts for ...异常并中止构建;_check_unsatisfied():检查requires中是否有未满足的依赖;_check_target_match():校验所有应用与当前硬件目标(如f7)兼容;_group_plugins():把PLUGIN类型应用挂到其宿主应用(requires指向的应用)名下,支持fal_embedded内嵌分发。
因此,conflicts会在构建早期以"硬失败"方式生效,而requires/provides会被自动补全——这也是provides与requires功能等同的原因:依赖方向由构建集闭合过程统一处理。
.fam 文件内容:一份清单可声明多个应用
.fam文件可包含一个或多个应用定义。例如 applications/services/bt/application.fam 中的一部分:
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", "bt_settings.h", "bt_service/bt_settings_api_i.h", ], ) App( appid="bt_start", apptype=FlipperAppType.STARTUP, entry_point="bt_on_system_start", order=40, )这个真实例子展示了多个关键点:
- 同一个
.fam可以包含 SERVICE、STARTUP 等多个App()调用; bt服务通过provides声明它提供了bt_start与bt_settings两个应用,任何requires=["bt_start"]或requires=["bt_settings"]的应用都能自动把bt拉进构建集;sdk_headers把bt_service/bt.h等头文件暴露给外部应用作为 API。
仓库中的典型用法参考
编写自己的清单前,可以对照这些真实案例:
- DEBUG 应用:applications/debug/accessor/application.fam 展示了
apptype=FlipperAppType.DEBUG、targets=["f7"]、requires=["gui"]、fap_category="Debug"的组合。 - EXTERNAL 应用与 PLUGIN 集群:applications/system/js_app/application.fam 是一个典型示例:
js_app本体是EXTERNAL(带fap_icon、fap_category="assets"、fap_libs=["assets"]、明确的sources列表),而cli_js、js_event_loop、js_gui等一批PLUGIN都requires=["js_app"],形成"宿主 + 插件"结构;js_vgm甚至用sources=["modules/js_vgm/*.c", "modules/js_vgm/ICM42688P/*.c"]演示了带目录的通配符收集。 - MENUEXTERNAL + 内嵌插件:applications/main/nfc/application.fam 展示了
FlipperAppType.MENUEXTERNAL与fal_embedded=True的配合,以及用"!plugins"、"!cli"、"!mosgortrans"、"!gallagher"、"!nfc_emv_parser.c"、"!*_extra_scenes.c"排除子目录/文件的sources写法(注意:排除规则匹配目录/文件名本身而非完整路径)。 - 构建集配置:fbt_options.py 中的
FIRMWARE_APPS定义了"default"与"unit_tests"两个预设,按 appid 引用应用集合;FIRMWARE_APP_SET选择生效的预设。这也解释了appid作为构建系统主键的作用。
编写清单的常见错误与排查
结合源码校验逻辑,以下几点最容易踩坑:
appid不合规:必须匹配^[a-z0-9_]+$,否则抛出Invalid appid ...;- 重复
appid:整个固件中 appid 必须唯一,否则报Duplicate app declaration; - PLUGIN 设置
stack_size:会提示改为FlipperAppType.EXTERNAL;PLUGIN还必须有requires; - 内置应用使用 FAP 专属参数:如非外部类型设置
fap_extbuild/fap_private_libs会被拒绝;外部类型设置resources同样会被拒绝; .fam为空:文件执行后没有任何App()定义会被视为 malformed;- C++ 入口点:
entry_point指定的 C++ 函数必须用extern "C"包裹,否则链接时因名字修饰找不到符号; - 栈大小权衡:
stack_size过小导致栈溢出崩溃,过大浪费堆内存,可用top/freeCLI 实测后再定。
掌握 FAM 语法与校验规则后,你就可以为固件新增组件、把应用打包成 FAP、组织插件集群,并放心地让fbt在构建时自动解析依赖、检测冲突,最终生成你想要的固件形态。
【免费下载链接】unleashed-firmwareFlipper Zero Unleashed Firmware项目地址: https://gitcode.com/GitHub_Trending/un/unleashed-firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考