8、升级脚本与Edify语法:updater-script编写规则、Edify函数详解
OTA升级的核心,说白了就是一套「指令集」。系统拿到升级包后,得知道先干什么、后干什么。这个指令集就是 updater-script,它用 Edify 语言写成。我刚开始接触这块时,觉得它不就是个脚本嘛,后来踩了几个坑才明白——这玩意儿写不好,设备就变砖了。
updater-script 编写规则
每个 OTA 包里的 META-INF/com/google/android/updater-script 文件,就是升级脚本。它按行执行,每行一个 Edify 函数调用。规则其实不多,但每条都很关键:
- 顺序执行:脚本从上往下跑,没有跳转、循环这些花哨的东西。所以分区挂载、数据擦除、文件解压的顺序必须严格。
- 返回值决定成败:每个函数执行后,如果返回 false,整个升级流程就中止了。系统会尝试回滚。
- 分号结尾:每条语句末尾必须加分号,少一个都不行。我早期犯过这种低级错误,编译出来的包刷进去直接报错。
- 注释用 #:不支持 /* */ 这种多行注释,老老实实用 # 吧。
核心原则:updater-script 不是 shell 脚本,它是 Edify 解释器逐条解析执行的。别想着在里面写 for 循环或者 if-else 嵌套太深,Edify 的条件判断能力有限。
Edify 函数详解
Edify 提供了几十个内置函数,但日常 OTA 升级常用的就那么几个。我挑最核心的讲,这些你写脚本时基本都会用到。
mount — 挂载分区
升级前必须先挂载分区,不然你往哪儿写数据?函数签名是这样的:
mount(fs_type, partition_type, device_path, mount_point);
举个例子:
mount("ext4", "EMMC", "/dev/block/by-name/system", "/system");
参数说明:
fs_type:文件系统类型,常见 ext4、f2fs。partition_type:分区类型,EMMC 或 MTD。现在基本都是 EMMC。device_path:设备节点路径,比如/dev/block/by-name/system。mount_point:挂载点,比如/system。
个人习惯:我一般会在脚本开头把所有要操作的分区一次性挂载好,避免后面反复挂载。但要注意,挂载前确保分区没有被占用,否则会失败。
format — 格式化分区
这个函数用来清空分区数据。注意,它不只是删除文件,而是重建文件系统。签名如下:
format(fs_type, partition_type, device_path, mount_point, fs_options);
实际用法:
format("ext4", "EMMC", "/dev/block/by-name/system", "/system", "");
我曾经在项目里犯过一个错:format 之后忘了重新挂载分区,结果后面 package_extract_file 写数据时找不到挂载点,升级直接失败。记住,format 会卸载分区,完事儿后得重新 mount。
delete — 删除文件
删除指定路径的文件或目录。支持通配符,但别指望它像 rm -rf 那么强大。
delete("/system/app/SomeApp.apk");
delete("/data/dalvik-cache");
注意:delete 只能删除已挂载分区上的文件。如果路径不存在,它不会报错,直接返回 true。这个特性有时候会坑人——你以为删了,其实路径写错了。
package_extract_file — 解压文件
从 OTA 包中提取文件到指定位置。这是升级的核心操作,签名有两种形式:
package_extract_file("source_path", "dest_path");
package_extract_file("source_path"); // 解压到当前目录
示例:
package_extract_file("system/build.prop", "/system/build.prop");
package_extract_file("boot.img", "/dev/block/by-name/boot");
第二个参数可以是设备节点,直接写入分区。刷 boot、recovery 时就是这么干的。
避坑指南:解压前确保目标路径的父目录存在。如果父目录不存在,函数会失败。我一般会在解压前用 run_program("/sbin/mkdir", "-p", "/some/path") 先建好目录。
条件判断与动态分区
Edify 的条件判断能力有限,但够用。它通过 ifelse 函数实现:
ifelse(condition, true_branch, false_branch);
condition 通常是一个返回布尔值的函数调用,比如:
ifelse(
getprop("ro.product.device") == "sailfish",
package_extract_file("system_sailfish.img", "/dev/block/by-name/system"),
package_extract_file("system_common.img", "/dev/block/by-name/system")
);
这段代码的意思是:如果设备是 sailfish,就刷 sailfish 的系统镜像,否则刷通用镜像。这在多设备共用 OTA 包时非常有用。
动态分区是 Android 10 之后引入的。它让分区大小不再固定,而是由 super 分区统一管理。在 updater-script 里,动态分区操作依赖 update_dynamic_partitions 函数:
update_dynamic_partitions(package_extract_file("dynamic_partitions_op_list"));
这个函数会读取一个操作列表文件,里面描述了如何调整分区大小、创建或删除逻辑分区。我刚开始接触动态分区时,觉得它很复杂,后来发现其实就是把分区管理的逻辑从脚本里抽出去了,反而更清晰。
动态分区下的 mount 写法也有变化:
mount("ext4", "EMMC", "/dev/block/by-name/system", "/system", "");
嗯,看起来和普通分区一样,但背后的设备节点其实是 dm-linear 映射出来的。你不需要关心底层细节,Edify 会处理好。
我建议:如果你的项目用了动态分区,务必在脚本开头调用 update_dynamic_partitions,然后再 mount。顺序错了,分区可能还没创建好,mount 会失败。
知识体系总览
下面这张图把本章的核心逻辑串起来了。你写脚本时,照着这个流程走,基本不会出大问题。
这张图展示了从挂载分区到条件判断、再到文件解压和清理的完整链路。你写脚本时,每一步都要想清楚:分区挂了吗?条件判断对吗?解压路径存在吗?
好了,关于升级脚本和 Edify 语法,核心内容就这些。记住,写脚本时多想想边界情况——设备空间不够怎么办?分区挂载失败怎么办?把这些想清楚了,你的 OTA 升级才能稳如老狗。
公众号:蓝海资料掘金营,微信deep3321