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 会失败。

知识体系总览

下面这张图把本章的核心逻辑串起来了。你写脚本时,照着这个流程走,基本不会出大问题。

updater-script 执行流程 开始执行 updater-script 步骤1:mount 所有目标分区 mount("ext4", "EMMC", "/dev/block/...", "/system") 步骤2(可选):update_dynamic_partitions 仅动态分区设备需要,普通分区跳过 条件判断 ifelse(getprop(...) == "...", ...) 分支A:刷A镜像 package_extract_file 分支B:刷B镜像 package_extract_file 步骤4:delete 清理 + 收尾 delete("/system/app/OldApp.apk")

这张图展示了从挂载分区到条件判断、再到文件解压和清理的完整链路。你写脚本时,每一步都要想清楚:分区挂了吗?条件判断对吗?解压路径存在吗?

好了,关于升级脚本和 Edify 语法,核心内容就这些。记住,写脚本时多想想边界情况——设备空间不够怎么办?分区挂载失败怎么办?把这些想清楚了,你的 OTA 升级才能稳如老狗。


公众号:蓝海资料掘金营,微信deep3321