edify script 文法大全
注意: 以下の記述は Froyo (2.2) 時点のソースに基づく
リカバリモードのアップデータ (update.zip
) で使われるアップデータスクリプトは Donut (1.6) 以降 edify というミニスクリプトが採用されている。
edify の文法はおおまかに次のような特徴をもつ。
- 一見 C 風*1だが異なる部分も多い
- 型はおもに文字列。ほかに BLOB (ファイルの中身) と NULL も一応ある (NULL はほとんど使われていない)
- 変数はない
- ユーザ定義の関数やプロシージャもない
文法については bootable/recovery/edify/README
にて説明されている。
bootable/recovery/edify/README
の訳
donut 以降では edify という新しい簡易スクリプト言語でアップデートスクリプトを書く。
edify は以前のスクリプトシステム amend にぱっと見似ているが異なるものである。
edify の簡潔な概要:
- スクリプト全体が (訳注: 最終的に) 単一の「式」として評価される。
- すべての「式」の値は文字列である。
- 文字列リテラルはダブルクォートで表記される。
\n
,\t
,\"
,\\
のエスケープは適宜解釈される。
また\x4a
のような16進エスケープも解釈される (訳注: 10進エスケープは解釈されない)。 - 英数字, コロン, アンダーバー, スラッシュ, ピリオドのみからなるトークンは文字列として解釈されるのでダブルクォートは必要ない。
- ただし以下の単語は予約語である。
if then else endif
クォーテーションなしでは特別な意味をもつ。
(もちろんダブルクォートされたものは単に文字列リテラルとなる) - 真偽値として評価される場合,空文字列は「偽」であり,その他の文字列は「真」である。
(訳注: 慣例的に文字列 "t
" を「真」として返す関数が多い) - すべての (訳注: 組込) 関数は実際には (LISP 的意味合いで) マクロである。関数本体がどの引数を実際に評価するかコントロールすることができる。このことは関数が制御構造としても振る舞えることを示している。
訳注:foo(bar(), baz())
みたいな関数foo()
があった際,bar()
とbaz()
は実際に必要なときに評価されるということ。
もっとわかりにくいか。実際に存在する関数で具体例を書くと,ifelse("t", bar(), baz());
このコードではbar()
は評価されるけどbaz()
は評価されない。C などの言語では引数は関数の実行前に評価されてしまうよね。
いくつかの例:
- クォートされた文字列とクォートされていない文字列に区別はない。
文字列中にホワイトスペースを含めたいときにはクォーテーションは必要になる。
以下の式はすべて同じ文字列として評価される。
"a b" a + " " + b "a" + " " + "b" "a\x20b" a + "\x20b" concat(a, " ", "b") "concat"(a, " ", "b")
最後の例からもわかるように,関数名もただの文字列にすぎない。しかしながら関数名は単一の文字列「リテラル」でなくてはならない。
以下は間違い:
("con" + "cat")(a, " ", b) # 文法エラー
ifelse()
組込関数は3つの引数 (訳注: 後述のように2つでもよい) をとる。1つ目の引数の真偽によって2つ目か3つ目かの必ずどちらかの引数だけ評価する。if
/else
文のように見えるシンタックスシュガーも用意されている。
# 以下はすべて同じ意味 ifelse(something(), "yes", "no") if something() then yes else no endif if something() then "yes" else "no" endif
else 部分はオプショナルである。
if something() then "yes" endif # something() が偽の場合, # 評価値は偽となる ifelse(condition(), "", abort()) # condition() が偽のときのみ # abort() が呼ばれる
最後の例は以下と同義である。
assert(condition())
&&
と||
演算子は同時に使用することができる。いずれも右辺式は式の真偽値が必要となる場合だけ評価される。式全体の値は最後に評価された式の値となる:
file_exists("/data/system/bad") && delete("/data/system/bad") file_exists("/data/system/missing") || create("/data/system/missing") get_it() || "xxx" # get_it() の評価値が真の場合はそれを返す # さもなくば "xxx" を返す
- 「
;
」の目的はもちろん命令文((訳注: 原文は imperative statements。訳出が難しかったので訳者個人の考えを書く。edify 言語系自体は関数型言語的である。だがセミコロン演算子 (;
) を用いると,命令型プログラミング (≒手続き型言語) のように逐次実行的な記述を (機能的にも,見た目的にも) 行うことができる。というようなことをいいたいのではないかと思う。))の機能を提供するためである。だがこの演算子はどこでも使うことができる。
式全体の値は右辺の値となる:
concat(a;b;c, d, e;f) # 評価値は "cdf"
より有用な例:
ifelse(condition(), (first_step(); second_step();), # 2つ目の「;」はオプショナル alternative_procedure())
訳注: ifelse()
の2番目の引数は演算子の結合順位的には必ずしもカッコで囲う必要がない気がする
訳者による補足
文法を規定するレキサやパーサの設定は edify/lexer.l
および edify/parser.y
を読めばわかる。
- 「
#
」以降はコメントとなり無視される。 - ダブルクォーテーションされた文字列は途中で改行していても継続する。
- 空文字列以外は「真」として評価される。ホワイトスペースや改行や "
0
" も真である。 - ほとんどの組込関数において,整数値として評価される場合,基数 10 の
strtol()
を利用している。このため,これらの引数において8進数表記 (0123
) や16進数表記 (0xbeaf
) は使えない。 - BLOB (ファイルの中身) 型は評価時に文字列化されない。文字列等が必要な場面 (たいていの関数の引数など) で BLOB を与えるとエラーとなる。
### 演算子 expr + expr # 文字列として結合 expr == expr # 文字列として比較 expr != expr # 文字列として比較 expr && expr # 論理積; 値を返す; 左辺が偽の場合,右辺は評価されない expr || expr # 論理和; 値を返す; 左辺が真の場合,右辺は評価されない ! expr # 論理否定 ### 制御構造 if cond_expr then expr else expr endif # elsif 系はない; 自力でネストさせる必要あり if cond_expr then expr endif # else を省略した場合,cond_expr が偽のときの # 式の値は cond_expr の評価値となる # つまり偽のときは戻り値は実質 "" となる
組込関数
アップデータで利用するさまざまな機能が組込関数として実装してある*2。組込関数自体は RegisterFunction(name, method)
((bootable/recovery/edify/expr.c (353)
)) 関数で定義できるので,それで検索すればどのような組込関数が提供されているかわかる。
組込関数は以下の3箇所で登録されている。
- 言語系に組み込まれているもの((
bootable/recovery/edify/expr.c (384): RegisterBuiltins()
にて定義)) - アップデートバイナリ (
updater-binary
) に組み込まれているもの((bootable/recovery/updater/install.c (1006): RegisterInstallFunctions()
にて定義)) - ベンダ提供のもの
BoardConfig.mk
などでTARGET_RECOVERY_UPDATER_LIBS
に定義されたライブラリに記述されているもの- 各ライブラリに
Register_libname()
なる関数を実装し,その中で上述のRegisterFunction(name, method)
を呼び出して組込関数を登録するbootable/recovery/updater/Android.mk
や HTC のベンダライブラリlibrecovery_updater_htc
(のソースdevice/htc/common/updater/recovery_updater.c
) を参照のこと
言語系の組込関数
ifelse(cond, on_true, on_false) |
|
ifelse(cond, on_true) |
|
abort([msg]) |
abort |
assert(cond_a, cond_b, cond_c, ...) |
assert |
concat(expr_a, expr_b, expr_c, ...) |
文字列として連結 |
is_substring(search_pattern, whole_str) |
部分文字列判定 |
stdout(expr_a, expr_b, expr_c, ...) |
標準出力に出力 |
sleep(seconds) |
sleep |
less_than_int(expr_a, expr_b) |
expr_a < expr_b |
greater_than_int(expr_a, expr_b) |
expr_a > expr_b |
アップデートバイナリの組込関数
ファイルパスを受け取るほとんどの関数は,物理的にその時点でアクセス可能なパスである必要がある。つまり update.zip
アーカイブ内のファイルは対象とならない。ただし package_extract_dir()
, package_extract_file()
関数の第1引数はアーカイブ内のファイルパスを示す。
mount(type, location, mount_point) |
マウントする |
is_mounted(mount_point) |
マウントされているか判定 |
unmount(mount_point) |
アンマウントする |
format(type, location) |
MTD パーティションをフォーマットする |
show_progress(portion, sec) |
プログレスバーの表示 |
set_progress(frac) |
プログレスバーの進捗セット |
delete(path1, path2, ...) |
ファイル削除 |
delete_recursive(path1, path2, ...) |
ファイル・ディレクトリ削除 |
package_extract_dir(package_path, target_path) |
update.zip からディレクトリを展開 |
package_extract_file(package_path, target_path) |
update.zip からファイルを展開 |
package_extract_file(package_path) |
update.zip からファイルを展開して返す |
symlink(src, target1, target2, ...) |
シンボリックリンクを張る |
set_perm(uid, gid, mode, path1, path2, ...) |
パーミッションを設定 |
set_perm_recursive(uid, gid, dir_mode, file_mode, |
パーミッションを設定 再帰版 |
getprop(key) |
getprop |
file_getprop(filename, key) |
ファイルが対象の getprop |
write_raw_image(filename, partition) |
イメージファイルを MTD パーティションに書き込む |
apply_patch(src_file, target_file, target_sha1, target_size, |
差分の適用 |
apply_patch_check(file, sha1_1, sha2_1, ...) |
差分適用前チェック |
apply_patch_space(bytes) |
差分適用に必要な空き容量のチェック |
read_file(filename) |
ファイルを読み込み返す |
sha1_check(data) |
SHA1 の計算 |
sha1_check(data, sha1_hex, sha1_hex, ...) |
SHA1 のチェック |
ui_print(str1, str2, ...) |
recovery UI に表示 |
run_program(prog, arg1, arg2, ...) |
外部コマンドの実行 |
組込関数詳説
ifelse(cond, on_true, on_false)
略
ifelse(cond, on_true)
略
abort([msg])
評価した時点でアボートする。recovery updater 環境では,UI にメッセージ msg
を出力する。
assert(cond_a, cond_b, cond_c, ...)
与えられた全 cond
が真でない場合,アボートする。"assert failed: 偽となった部分" のようなメッセージをアボート出力する。
concat(expr_a, expr_b, expr_c, ...)
全引数を結合した文字列を返す。
is_substring(search_pattern, whole_str)
search_patter
が whole_str
の部分文字列なら真 ("t"
) を返す。
sleep(seconds)
指定された秒数 sleep する。
- 戻り値
seconds
less_than_int(expr_a, expr_b)
両辺が数値として評価される。expr_a < expr_b
なら真を返す。
greater_than_int(expr_a, expr_b)
両辺が数値として評価される。expr_a > expr_b
なら真を返す。
mount(type, location, mount_point)
指定されたパーティションを指定したファイルタイプでマウントする。マウントオプションはつけることができない。
type
に "MTD"
を指定すると,location
に MTD パーティション名 (userdata
など) を指定することができる。ただしこの場合,ファイルタイプは yaffs2 固定となる。
マウントの前にあらかじめ mkdir(mount_point, 0755);
しておいてくれる。またマウントに失敗しても fatal error とはならない。
- 戻り値
- 成功時は
mount_point
,失敗時は偽 (""
) - 例
mount("MTD", "system", "/system");
mount("vfat", "/dev/block/mmcblk0", "/sdcard");
is_mounted(mount_point)
現在マウントされているかどうかを返す。
- 戻り値
- 真の場合は
mount_point
,偽の場合は空文字列 (""
) - 例
is_mounted("/system")
unmount(mount_point)
指定されたマウントポイントをアンマウントする。
実は umount()
の成否はわからない
- 戻り値
is_mounted()
でない場合は偽 (""
),is_mounted()
の場合はmount_point
- 例
unmount("/system");
format(type, location)
指定された MTD パーティションをフォーマットする。
type
は "MTD" のみサポート。
- 戻り値
location
- 例
format("MTD", "system");
show_progress(portion, sec)
プログレスバーを表示する (後述)。
portion
は 0
〜 1.0
の小数値。sec
はバーが伸びるのにかかる時間 (秒数; 整数値)。
(ホスト側で実行)
- 戻り値
portion
- 例
show_progress(0.3, 10);
set_progress(frac)
プログレスバーの進捗を設定する (後述)。
frac
は 0
〜 1.0
の小数値。
(ホスト側で実行)
- 戻り値
frac
- 例
set_progress(0.8);
delete(path1, path2, ...)
指定された全パス(ファイルのみ)を削除する。
- 戻り値
- 正常に削除できた個数
- 例
delete("/tmp/foo.txt", "/tmp/bar.bin");
package_extract_dir(package_path, target_path)
指定されたZIP内ディレクトリを指定されたディレクトリに展開する。
ctime や mtime は必ず 2008/8/1 になるようだ。
- 戻り値
- 真偽値
- 例
package_extract_dir("system", "/system");
package_extract_file(package_path, target_path)
指定された ZIP 内ファイルを指定されたファイル名で展開する。
- 戻り値
- 真偽値
- 例
package_extract_file("hoge.txt", "/tmp/hoge.txt");
package_extract_file(package_path)
指定された ZIP 内ファイルを展開し内容を返す。
戻り値が VAL_BLOB
タイプなので sha1_check()
か apply_patch()
で使うしかない。
- 戻り値
- ファイルの内容 (
VAL_BLOB
タイプ); 失敗時はVAL_NULL
。 - 例
package_extract_file("hoge.txt")
symlink(src, target1, target2, ...)
target1
等が既存の場合は削除してシンボリックリンクを貼り直してくれる。
symlink(2)
と同様の挙動を示すことに注意。つまり,相対パスの場合,各ターゲットからの相対パスとなる (カレントディレクトリは関係ない)。
- 戻り値
- 空文字列 (
""
; 偽) - 例
symlink("busybox", "/system/bin/ls", "/system/bin/ps");
set_perm(uid, gid, mode, path1, path2, ...)
指定されたパス群にパーミッション, オーナ, グループを設定する。
めずらしく基数 0 の strtoul()
を使用しているので,引数には8進数記法(0777
等)や16進数記法(0x1ff
等)も使えるはず。
- 戻り値
- 空文字列 (
""
; 偽) - 例
set_perm(0, 0, 04755, "/system/bin/su");
set_perm_recursive(uid, gid, dir_mode, file_mode, path1, path2, ...)
指定されたパス群を再帰的にパーミッション等を設定する。ディレクトリとファイルで別のパーミッションを指定することが可能。
- 戻り値
- 空文字列 (
""
; 偽) - 例
set_perm_recursive(0, 0, 0755, 0644, "/system");
getprop(key)
getprop コマンドと同様。
- 例
getprop("ro.product.model")
file_getprop(filename, key)
指定されたファイルをプロパティファイルとみなしてプロパティ値を取得する。
- 例
file_getprop("/system/build.prop", "ro.build.id")
write_raw_image(filename, partition)
指定された MTD パーティションにファイルの内容を書きこむ。
- 戻り値
- 成功時
partition
- 例
write_raw_image("/sdcard/boot.img", "boot");
apply_patch(src_file, target_file, target_sha1, target_size, sha1_1, patch_1, ...)
指定された src_file
をもとに patch を差分適用して target_file
を生成する (applypatch
コマンドと同様)。
patch_1
等は VAL_BLOB
タイプである必要がある。つまり read_file()
や package_extract_file()
の戻り値を使う必要がある。
applypatch
と同じく src_file
や target_file
には MTD:partition_name
記法が使えるようだ (未確認)。
target_file
に「-
」を指定すると src_file
を上書きする。すでに target_sha1
の target_file
が存在する際はなにもしない。
- 戻り値
- 真偽値
- 例
apply_patch("foo.txt", "bar.txt", "cafe....", 1024, "beaf....", package_extract_file("patch.p"));
apply_patch_check(file, sha1_1, sha2_1, ...)
指定されたファイルの SHA1SUM に一致する引数があるか調べる。
- 戻り値
- 真偽値
- 例
apply_patch_check("foo.txt", "cafe....", "beaf....")
apply_patch_space(bytes)
bytes
バイト分の空き容量を /cache
パーティションに用意する。
- 戻り値
- 真偽値 (用意できなかった場合,偽)
- 例
apply_patch_space(65536);
read_file(filename)
ファイルを読み込んで内容を返す。ファイルを読み込めなかった場合アボートする。
ただし戻り値は VAL_BLOB
型なので sha1_check()
か apply_patch()
で使うしかない
- 戻り値
- ファイルの内容 (
VAL_BLOB
型) - 例
read_file("/system/etc/hosts")
sha1_check(data)
data
の SHA1SUM を算出して返す。
data
は通常の文字列型の値でも VAL_BLOB
型の値でもどちらでもよい。
- 戻り値
- SHA1 digest 値
- 例
assert( sha1_check(read_file("/system/etc/hosts")) == "..." );
sha1_check(data, sha1_hex1, sha1_hex2, ...)
data
の SHA1SUM を算出し,後続の sha1_hex
にマッチするものがあるかどうか調べる。
- 戻り値
- 真偽値
- 例
sha1_check(read_file("/system/etc/hosts"), "a1..", "3e..");
ui_print(str1, str2, ...)
recovery UI に文字列を出力する。
(ホスト側で実行)
- 戻り値
- 結合された文字列
- 例
ui_print("Hello, World!\n\nThis is test.", "foo bar");
run_program(prog, arg1, arg2, ...)
外部プログラムを実行する。
- 戻り値
- 終了コード
- 例
run_program("/system/bin/sleep", "3");
show_progress()
と set_progress()
について
プログレスバーまわりは update-binary
側ではなく,ホスト側 (recovery
) で実際に処理をおこなっているので,挙動を知るにはホスト側のコード((show_progress()
はホスト側のコード install.c (123): try_update_binary()
で駆動されている。実際の表示にまつわるコードは ui.c (358): ui_show_progress()
に存在する。set_progress()
はホスト側のコード install.c (132): try_update_binary()
で駆動されている。実際の表示にまつわるコードは ui.c (371): ui_set_progress()
に存在する。))を読む必要がある。
両者の違いはわかりにくい。show_progress(portion, sec)
はこれから行う作業量の全体に対するだいたいの割合 (portion
) と見積もっている時間 (sec
) を指定する((そうするとプログレスバーが sec
秒をかけて portion
分徐々に伸長していく。))。set_progress(frac)
はその作業の途中でどこまで作業が進んだかを指定する。複数 show_progress()
を設定する場合,その portion
の和は 1.0
にするべきである。
実例をあげる。
### 20秒ほどかかる作業; 全体の50% show_progress(0.5, 20); ... # ちょっとした作業 set_progress(0.3); ... # ちょっとした作業 set_progress(0.7); ... # ちょっとした作業 (以下略) set_progress(1.0); ### 5秒ほどかかる作業; 全体の20% show_progress(0.2, 5); set_progress(0.2); set_progress(0.4); set_progress(0.6); set_progress(1.0); ### 10秒ほどかかる作業; 全体の30% show_progress(0.3, 10); set_progress(0.1); set_progress(0.9); set_progress(1.0);
show_progress()
すると前回のプログレスバーの最終端までジャンプするので,実際には set_progress(1.0)
は (最後のものを除いて) 必要ない。
また細かいことであるが,show_progress()
の portion
の和を 1.0
にするようにと書いたが,実際のアップデートプロセスの前にアップデートパッケージ (update.zip
) の検証フェーズがあり,その検証フェーズが全体の 25% であるとデフォルトで設定((VERIFICATION_PROGRESS_FRACTION = 0.25
, TIME = 60
))されている。よって,show_progress(0.4, 10);
のように指定した場合,指定したフェーズのプログレスバーは全体の 40% 分伸長するのではなく 40% * 75% = 30% 分伸長する。なんにしても portion
の総和が 1.0
となるように設計するべきであることには変わらない。
apply_patch()
について
apply_patch( "source.img", "target.img", "cafe...", 1024, "babe...", "patch1.p", "beef...", "patch2.p" );
のようなスクリプトになっていた場合,
- すでに
target.img
が存在し,その SHA1SUM がcafe...
の場合,パッチ適用はスキップされる (終了; 真値) source.img
の SHA1SUM がbabe...
の場合,patch1.p
を適用してtarget.img
を生成する。適用後の SHA1SUM がcafe...
の場合,終了。違う場合,エラー終了 (偽値)source.img
の SHA1SUM がbeef...
の場合,patch2.p
を適用してtarget.img
を生成する。適用後の SHA1SUM がcafe...
の場合,終了。違う場合,エラー終了 (偽値)- 適用可能なパッチがない場合,エラー (偽値)
順番にパッチを当てたりするわけではないことに注意が必要である。ひとつパッチを当てた時点で,要求する target の SHA1SUM にならなければエラーとなる。
スクリプト例
Android 標準ビルドキットで生成*4したアップデータスクリプトを以下に掲載する。build/tools/releasetools/ota_from_target_files
という (Python 製) ツールの def WriteFullOTAPackage()
で生成されている。
assert(!less_than_int(1305679443, getprop("ro.build.date.utc"))); assert(getprop("ro.product.device") == "generic" || getprop("ro.build.product") == "generic"); show_progress(0.500000, 0); format("MTD", "system"); mount("MTD", "system", "/system"); package_extract_dir("recovery", "/system"); package_extract_dir("system", "/system"); symlink("toolbox", "/system/bin/cat", "/system/bin/chmod", "/system/bin/chown", "/system/bin/cmp", "/system/bin/date", "/system/bin/dd", "/system/bin/df", "/system/bin/dmesg", "/system/bin/getevent", "/system/bin/getprop", "/system/bin/hd", "/system/bin/id", "/system/bin/ifconfig", "/system/bin/iftop", "/system/bin/insmod", "/system/bin/ioctl", "/system/bin/ionice", "/system/bin/kill", "/system/bin/ln", "/system/bin/log", "/system/bin/ls", "/system/bin/lsmod", "/system/bin/mkdir", "/system/bin/mount", "/system/bin/mv", "/system/bin/nandread", "/system/bin/netstat", "/system/bin/newfs_msdos", "/system/bin/notify", "/system/bin/printenv", "/system/bin/ps", "/system/bin/reboot", "/system/bin/renice", "/system/bin/rm", "/system/bin/rmdir", "/system/bin/rmmod", "/system/bin/route", "/system/bin/schedtop", "/system/bin/sendevent", "/system/bin/setconsole", "/system/bin/setprop", "/system/bin/sleep", "/system/bin/smd", "/system/bin/start", "/system/bin/stop", "/system/bin/sync", "/system/bin/top", "/system/bin/umount", "/system/bin/vmstat", "/system/bin/watchprops", "/system/bin/wipe"); set_perm_recursive(0, 0, 0755, 0644, "/system"); set_perm_recursive(0, 2000, 0755, 0755, "/system/bin"); set_perm(0, 3003, 02750, "/system/bin/netcfg"); set_perm(0, 3004, 02755, "/system/bin/ping"); set_perm(0, 2000, 06750, "/system/bin/run-as"); set_perm(1002, 1002, 0440, "/system/etc/dbus.conf"); set_perm(1014, 2000, 0550, "/system/etc/dhcpcd/dhcpcd-run-hooks"); set_perm(0, 2000, 0550, "/system/etc/init.goldfish.sh"); set_perm(0, 0, 0544, "/system/etc/install-recovery.sh"); set_perm_recursive(0, 0, 0755, 0555, "/system/etc/ppp"); set_perm_recursive(0, 2000, 0755, 0755, "/system/xbin"); set_perm(0, 0, 06755, "/system/xbin/librank"); set_perm(0, 0, 06755, "/system/xbin/procmem"); set_perm(0, 0, 06755, "/system/xbin/procrank"); set_perm(0, 0, 06755, "/system/xbin/su"); set_perm(0, 0, 06755, "/system/xbin/tcpdump"); show_progress(0.200000, 0); show_progress(0.200000, 10); assert(package_extract_file("boot.img", "/tmp/boot.img"), write_raw_image("/tmp/boot.img", "boot"), delete("/tmp/boot.img")); show_progress(0.100000, 0); unmount("/system");