OTA 软件包内部探秘

系统从 bootable/recovery/updater 构建更新程序二进制文件并将其用于 OTA 软件包。

OTA 软件包本身是包含可执行二进制文件 META-INF/com/google/android/update-binary 的 .zip 文件(ota_update.zipincremental_ota_update.zip)。

更新程序包含多个内建函数和一个可扩展脚本语言 (Edify) 解释器,该脚本语言支持用于执行更新相关的典型任务的命令。更新程序会从 .zip 压缩包文件内的 META-INF/com/google/android/updater-script 文件中查找脚本。

注意:Edify 脚本和/或内建函数并不常用,但当您需要调试更新文件时会很有用。

Edify 语法

Edify 脚本是一个单一的表达式,其中的所有值都是字符串。从布尔值角度考虑,空字符串为 false,其他所有字符串均为 true。Edify 支持以下运算符(具有常规含义):

(expr )
 expr + expr  # string concatenation, not integer addition
 expr == expr
 expr != expr
 expr && expr
 expr || expr
 ! expr
 if expr then expr endif
 if expr then expr else expr endif
 function_name(expr, expr,...)
 expr; expr

由 a-z、A-Z、0-9、_、:、/、. 组成的任何字符串,只要不属于保留字,均视为字符串字面值。(保留字为 if else then endif。)字符串字面值也可以用英文双引号引起来;通过这种方法可以使用空格和上述集合之外的其他字符来创建值。\n、\t、\" 和 \\ 充当带英文引号的字符串中的转义字符,\x## 亦如此。

&& 和 || 为短路运算符;如果逻辑结果由左侧确定,则不评估右侧。下面两个表达式是对等的:

e1 && e2
if e1 then e2 endif

分号“;”运算符是序列点;表示要先评估左侧,再评估右侧。它的值是右侧表达式的值。分号也可以出现在表达式之后,其效果与 C 风格语句相仿:

prepare();
do_other_thing("argument");
finish_up();

内建函数

大部分更新功能都包含在可供脚本执行的函数中。(严格来说,这些是“宏”,而不是 Lisp 意义上的函数,因为它们不需要评估所有参数。)除非另有说明,函数在成功时返回 True,出错时返回 False。如果您希望在出错时中止脚本执行,请使用 abort() 和/或 assert() 函数。您也可以扩展更新程序中的可用函数集,以提供设备特定的功能

abort([msg])
立即中止脚本执行,并提供一条可选的 msg。如果用户开启了文本显示功能,msg 将出现在恢复日志和屏幕上。
assert(expr[, expr, ...])
依序评估每个 expr。如果出现 false,立即中止执行并显示“assert failed”和失败的表达式的源文本。
apply_patch(src_file, tgt_file, tgt_sha1, tgt_size, patch1_sha1, patch1_blob, [...])
将二进制补丁程序应用于 src_file 以生成 tgt_file。如果所需的目标文件与源文件相同,则传递“-”作为 tgt_file。tgt_sha1tgt_size 分别是目标文件的预期最终 SHA1 哈希值和文件大小。其余参数必须成对出现:一个 SHA1 哈希值(40 个字符的十六进制字符串)和一个 blob。如果源文件当前的内容具有给定的 SHA1,该 Blob 即为要应用的补丁程序。

修补以一种安全的方式完成,该方式可保证目标文件要么具有所需的 SHA1 哈希值和大小,要么不发生变化,而不会处于不可恢复的中间状态。如果在打补丁过程中进程中断,目标文件可能会处于中间状态;这时缓存分区中会存在一个副本,重新启动更新便可成功更新文件。

支持特殊语法将 MTD(内存技术设备)分区的内容作为文件处理,以便为引导分区这样的原始分区打补丁。由于 MTD 分区没有文件结束标记,因此要读取该分区,您必须知道您要读取的数据量。您可以使用字符串“MTD:partition:size_1:sha1_1:size_2:sha1_2”作为文件名来读取给定的分区。您必须至少指定一个 (size, sha-1) 对;如果您希望读取的内容有多种可能,则可以指定多个对。

apply_patch_check(filename, sha1[, sha1, ...])
如果 filename 的内容或 cache 分区中临时副本(如果存在)的内容具有与指定的 sha1 值之一相等的 SHA1 校验和,则返回 True。sha1 值指定为 40 位十六进制数字。该函数不同于 sha1_check(read_file(filename), sha1 [, ...]),因为它知道要检查 cache 分区副本,因此即使文件因 apply_patch() update 中断而损坏,apply_patch_check() 也会成功。
apply_patch_space(bytes)
如果至少有 bytes 个字节的暂存空间可用于应用二进制补丁,则返回 true。
concat(expr[, expr, ...])
评估每个表达式并将它们串联起来。在只有两个参数的特殊情况下,+ 运算符就是该函数的语法糖(但该函数形式可以包含任意数量的表达式)。表达式必须为字符串;该函数不能串联 Blob。
file_getprop(filename, key)
读取给定的 filename,将其解释为属性文件(例如:/system/build.prop),并返回给定 key 的值,如果 key 不存在,则返回空字符串。
format(fs_type, partition_type, location, fs_size, mount_point)
重新格式化给定的分区。支持的分区类型如下:
  • fs_type =“yaffs2”和 partition_type =“MTD”。Location 必须为 MTD 分区的名称;将在该位置构建一个空的 yaffs2 文件系统。其余参数未使用。
  • fs_type =“ext4”和 partition_type =“EMMC”。Location 必须为该分区的设备文件。将在该位置构建一个空的 ext4 文件系统。如果 fs_size 为零,则文件系统将占用整个分区。如果 fs_size 为正数,文件系统将占用分区的前 fs_size 个字节。如果 fs_size 为负数,文件系统将占用分区中除最后 |fs_size| 个字节以外的所有字节。
  • fs_type =“f2fs” 和 partition_type =“EMMC”。Location 必须为该分区的设备文件。fs_size 必须为非负数。如果 fs_size 为零,文件系统将占用整个分区。如果 fs_size 为正数,文件系统将占用分区的前 fs_size 个字节。
  • mount_point 应为文件系统的未来挂载点。
getprop(key)
返回系统属性 key 的值(或者,如果未定义,则返回空字符串)。由 recovery 分区定义的系统属性值未必与主系统属性值相同。此函数返回 recovery 分区中的值。
greater_than_int(a, b)
当且仅当 (iff) a(解析为整数)大于 b(解析为整数)时,才返回 True。
ifelse(cond, e1[, e2])
评估 cond,如果为 True,则评估并返回 e1 的值,否则评估并返回 e2(如果存在)。“if ... else ... then ... endif”结构就是此函数的语法糖。
is_mounted(mount_point)
当且仅当 mount_point 挂载了文件系统时,才返回 True。
is_substring(needle, haystack)
当且仅当 needle 是 haystack 的子字符串时,才返回 true。
less_than_int(a, b)
当且仅当 a(解析为整数)小于 b(解析为整数)时,才返回 true。
mount(fs_type, partition_type, name, mount_point)
在 mount_point 挂载 fs_type 的文件系统。partition_type 必须为以下类型之一:
  • MTD:name 是 MTD 分区的名称(例如:system、userdata;如需获取完整列表,请参见设备上的 /proc/mtd)。
  • EMMC

默认情况下,恢复分区不挂载任何文件系统(如果用户正在手动从 SD 卡安装软件包,则 SD 卡除外);您的脚本必须挂载需要修改的所有分区。

package_extract_dir(package_dir, dest_dir)
从 package_dir 下的文件包中提取所有文件,并将它们写入 dest_dir 下相应的树形结构中。所有现有文件都将被覆盖。
package_extract_file(package_file[, dest_file])
从更新软件包中提取单个 package_file 并将其写入 dest_file,如有必要,覆盖现有文件。在没有 dest_file 参数的情况下,将软件包文件的内容作为二进制 blob 返回。
read_file(filename)
读取 filename 并将其内容作为二进制 blob 返回。
run_program(path[, arg, ...])
在 path 上执行二进制文件并传入 arg。返回程序的退出状态。
set_progress(frac)
在最近的 show_progress() 调用所定义的块内设置进度条的位置。frac 必须在 [0.0, 1.0] 范围内。进度条从不向后移动;向后移动的尝试会被忽略。
sha1_check(blob[, sha1])
blob 参数是 read_file() 返回的 blob 类型或 package_extract_file() 的单参数形式 blob。在没有 sha1 参数的情况下,此函数返回 blob 的 SHA1 哈希值(作为 40 位十六进制字符串)。在提供了一个或多个 sha1 参数的情况下,如果 SHA1 哈希值等于其中一个参数,该函数返回此 SHA1 哈希值;如果与任何一个参数都不相等,则返回空字符串。
show_progress(frac, secs)
在 secs 秒(必须为整数)内推动进度条向前移动下一个 frac 的长度。secs 可为 0,在这种情况下,进度条不会自动向前移动,而是通过使用上面所定义的 set_progress() 函数实现进度递增。
sleep(secs)
休眠 secs 秒(必须为整数)。
stdout(expr[, expr, ...])
评估每个表达式并将其值转储到 stdout。这在调试时很有用。
tune2fs(device[, arg, …])
调整 device 上的可调参数 arg。
ui_print([text, ...])
串联所有 text 参数并将结果输出到界面(如果用户已开启文本显示功能,界面上将显示该结果)。
unmount(mount_point)
将挂载在 mount_point 上的文件系统卸载。
wipe_block_device(block_dev, len)
擦除给定块设备 block_dev 的 len 个字节。
wipe_cache()
可实现在安装过程成功结束时擦除 cache 分区。
write_raw_image(filename_or_blob, partition)
将 filename_or_blob 中的映像写入 MTD 分区。filename_or_blob 可以是一个指代本地文件的字符串或一个包含要写入的数据的 blob 值参数。如需将文件从 OTA 软件包复制到分区,请使用:write_raw_image(package_extract_file("zip_filename"), "partition_name");

注意:对于 Android 4.1 之前的版本,仅接受 filename。因此,如需完成该操作,必须先将数据解压到临时本地文件中。