Deno1.6.0 で入った deno compile のコードを読んでみる


Deno (ディノ) Advent Calendar 2020、22日目の記事です。

先日2020/12/8に[email protected]がリリースされ、この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つをくっつけているような感じになっていました。

https://github.com/denoland/deno/blob/2e74f164b6dcf0ecbf8dd38fba9fae550d784bd0/cli/standalone.rs#L164

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で実行したのと同じ結果になっています。

ここで、先程のシングルバイナリにまとめているコードを再掲すると、

https://github.com/denoland/deno/blob/2e74f164b6dcf0ecbf8dd38fba9fae550d784bd0/cli/standalone.rs#L164

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になる。

ということでした。面白い仕組みですね。