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 などの言語では引数は関数の実行前に評価されてしまうよね。
  • ("&&" や "||" のような) 演算子は組込関数のシンタックスシュガーにすぎない。よってそれらは制御構造として働くこともできる。
    訳注: たとえば foo() || bar()foo() が真の場合 bar() は評価 (実行) されない。なので制御構造的に使える。また実際には &&|| に対応する組込関数が存在するわけではない。
いくつかの例:
  • クォートされた文字列とクォートされていない文字列に区別はない。
    文字列中にホワイトスペースを含めたいときにはクォーテーションは必要になる。
    以下の式はすべて同じ文字列として評価される。
"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() にて定義))
    • 実際のアップデータスクリプトのパース・実行はこのアップデートバイナリによっておこなわれる
      • よって edify アップデートスクリプトを利用する場合,update.zip アーカイブの中に META-INF/com/google/android/update-binary バイナリを組み入れておく必要がある
      • 一部機能はアップデートバイナリ側ではなくホスト側 (recovery プロセス) によって実装されている
    • アップデートバイナリのソースは bootable/recovery/updater/ 以下に存在する
  • ベンダ提供のもの
    • 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,
path1, path2, ...)
パーミッションを設定 再帰
getprop(key) getprop
file_getprop(filename, key) ファイルが対象の getprop
write_raw_image(filename, partition) イメージファイルを MTD パーティションに書き込む
apply_patch(src_file, target_file, target_sha1, target_size,
sha1_1, patch_1, ...)
差分の適用
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_patterwhole_str の部分文字列なら真 ("t") を返す。

stdout(expr_a, expr_b, expr_c, ...)

全引数を標準出力に出力する*3

戻り値
空文字列 ("")
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)

プログレスバーを表示する (後述)。
portion01.0 の小数値。sec はバーが伸びるのにかかる時間 (秒数; 整数値)。
(ホスト側で実行)

戻り値
portion
show_progress(0.3, 10);
set_progress(frac)

プログレスバーの進捗を設定する (後述)。
frac01.0 の小数値。
(ホスト側で実行)

戻り値
frac
set_progress(0.8);
delete(path1, path2, ...)

指定された全パス(ファイルのみ)を削除する。

戻り値
正常に削除できた個数
delete("/tmp/foo.txt", "/tmp/bar.bin");
delete_recursive(path1, path2, ...)

指定された全パス(ディレクトリ含む)を再帰的に削除する。

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_filetarget_file には MTD:partition_name 記法が使えるようだ (未確認)。
target_file に「-」を指定すると src_file を上書きする。すでに target_sha1target_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");

*1:カンマ・セミコロンや一部制御構造のシンタックスシュガーによって C ――というより ALGOL――の見た目を醸し出しているが,じっさいには LISP により近い。

*2:前述のようにユーザ定義関数は定義できない。

*3:recovery updater 環境下では意味がないと思う。

*4:Androidソースコードを取得してそのまま generic product をビルドしても生成はされないのでひと工夫する必要がある。