Deno (ディノ) Advent Calendar 2020、22日目の記事です。
先日2020/12/8にDeno@v1.6.0がリリースされ、このAdvent Calendarの別の記事でも変更点を解説されている方がいらっしゃいます。
1.6.0の新機能で個人的に「おっ」とおもった機能が deno compile
コマンドです。
スクリプトをシングルバイナリとして実行できるようにするコマンドで、自分が作ったスクリプトなどを配布するときに楽になるので良い機能だなーと思っていました。
ところで、実行するスクリプトはtypescriptなわけですから、どうやって実行ファイルをつくっているのか気になりソースコードを読んでみました。すると色々おもしろかったので、記事として残そうと思います。
ソースリーディング
まず、 deno compile
コマンドを実装してそうなところを検索してみます。 cliディレクトリの中に、cli/main.rs があったのでそれから見てみました。
async fn compile_command(
flags: Flags,
source_file: String,
output: Option<PathBuf>,
) -> Result<(), AnyError> {
if !flags.unstable {
exit_unstable("compile");
}
これが対象の関数のようです。 たしかに deno compile
は現状では --unstable
フラグをつけないと実行できないのでそれっぽいです。
するとこの関数の最後に
create_standalone_binary(bundle_str.as_bytes().to_vec(), output.clone())
.await?;
といかにもな関数があったので、この中のはずです。
cli/standalone.rs で定義されているそうです。
pub async fn create_standalone_binary(
mut source_code: Vec<u8>,
output: PathBuf,
) -> Result<(), AnyError> {
この関数を見てみると、出力ファイルに
- 実行ファイル(=deno自身)、
- 実行対象のスクリプト
- 謎のtrailer
の3つをくっつけているような感じになっていました。
final_bin.append(&mut original_bin);
final_bin.append(&mut source_code);
final_bin.append(&mut trailer);
なるほどなるほど、ハリボテの実行ファイルをつくって、その中にdeno本体や、スクリプトをいれて、実質 deno run スクリプト
的なことを実行しているんだろうなーと予測しました。
とはいえ、その方式だと、
その中にdeno本体や、スクリプトをいれて、実質
deno run スクリプト
的なことを実行しているんだろうなー
というコード自体は生成する必要があるはずです。なので、それを追ってみました・・・・が全然見つかりませんでした。
で、いろいろコード見ていくと、同じ cli/standalone.rsに try_run_standalone_binary
https://github.com/denoland/deno/blob/2e74f164b6dcf0ecbf8dd38fba9fae550d784bd0/cli/standalone.rs#L30
という関数がありそのコメントを読むと、
/// This function will try to run this binary as a standalone binary
/// produced by `deno compile`. It determines if this is a stanalone
/// binary by checking for the magic trailer string `D3N0` at EOF-12.
/// After the magic trailer is a u64 pointer to the start of the JS
/// file embedded in the binary. This file is read, and run. If no
/// magic trailer is present, this function exits with Ok(()).
以下意訳
このバイナリ(deno自体)をdeno compileで出力された実行ファイルとして実行する場合、マジック定数(
D3NO
(きっとDENO
なんでしょうね )) があるはずで、その後に実際のソースコードが埋め込まれている場所のアドレスがあるはずなので、それを実行して、終了します。もし、マジック定数がなければ、この関数は何もせず単にOk(())
を返します。
とありました。実際に、 cli/main.rs 側に try_run_standalone_binary
を呼び出している箇所がありました。
https://github.com/denoland/deno/blob/2e74f164b6dcf0ecbf8dd38fba9fae550d784bd0/cli/main.rs#L1213
if let Err(err) = standalone::try_run_standalone_binary(args.clone()) {
eprintln!("{}: {}", colors::red_bold("error"), err.to_string());
std::process::exit(1);
}
そう。つまり、 deno compile
で生成した実行ファイルは ほぼdeno なわけです。
では、どれぐらい ほぼdeno なのか実際に確かめてみます。
シングルバイナリ化対象スクリプト
対象のスクリプトはdenoの紹介でよく使われるスクリプトを使います
% deno run https://deno.land/std/examples/welcome.ts
Welcome to Deno 🦕
deno compile
で実行バイナリを生成
% deno --unstable compile https://deno.land/std/examples/welcome.ts --output sample
Check https://deno.land/std/examples/welcome.ts
Bundle https://deno.land/std/examples/welcome.ts
Compile https://deno.land/std/examples/welcome.ts
Emit sample
% ./sample
Welcome to Deno 🦕
sample というバイナリができました。実行してみると、実際にスクリプトをdenoで実行したのと同じ結果になっています。
ここで、先程のシングルバイナリにまとめているコードを再掲すると、
final_bin.append(&mut original_bin);
final_bin.append(&mut source_code);
final_bin.append(&mut trailer);
となっており、 original_bin
というのが deno の実行ファイルです。ですので、このsampleバイナリからdenoのサイズ分切り取ってファイルに保存してみましょう。
deno のサイズ確認
% ls -l $(which deno)
lrwxr-xr-x 1 pocari admin 29 12 16 00:12 /usr/local/bin/deno@ -> ../Cellar/deno/1.6.0/bin/deno
% ls -l /usr/local/Cellar/deno/1.6.0/bin/deno
-r-xr-xr-x 1 pocari staff 43496264 12 8 23:38 /usr/local/Cellar/deno/1.6.0/bin/deno*
ということで、 43496264
バイトだそうです。
バイナリ分割
deno自体のサイズが 43496264
なので、sampleバイナリの先頭 43496264
バイトを切り取ってファイルに保存してみます。
イメージは↓の original_bin
の部分だけ切り出す感じです。
% dd if=sample of=extracted_binary bs=43496264 count=1
1+0 records in
1+0 records out
43496264 bytes transferred in 0.039424 secs (1103288969 bytes/sec)
% ls
extracted_binary sample*
% file extracted_binary
extracted_binary: Mach-O 64-bit executable x86_64
%
切り出したファイルを file コマンドで調べてみると mac の実行形式のファイルになっているので、実行できそうです。
実行権限がついていないので、権限をつけて実行してみましょう。
% ./extracted_binary --version
deno 1.6.0 (release, x86_64-apple-darwin)
v8 8.8.278.2
typescript 4.1.2
% ./extracted_binary run https://deno.land/std/examples/welcome.ts
Welcome to Deno 🦕
はい。denoのバイナリが抽出されました。
念の為、 実際にdiffも取ってみましたが、全く同じもののようでした。
% diff $(which deno) ./extracted_binary
% echo $?
0
まとめ
- バージョン
1.6.0
からdeno compile
でシングルバイナリをつくることができるようになった。 -
deno compile
で生成されたバイナリは deno 自身のバイナリに実行時のソースなどの情報がくっついたものである。 - なので、生成されたバイナリから、先頭のdeno部分だけ切り取ると、denoになる。
ということでした。面白い仕組みですね。