🐖

Rustコンパイラ自体のビルドプロセスを理解する

2022/07/12に公開

この記事では、Rustコンパイラ (rust-lang/rust) がどういったプロセスでビルドされるかについて書いていく。(ここではあくまで細部を省いておおまかに理解することが目的。)

Bootstrapping

RustコンパイラはRustで書かれている。ではこのRustコンパイラをソースコードからビルドするためのRustコンパイラはどこから来るのだろうか。これはブートストラップ問題とか言われ、鶏と卵の話によく例えられる。

「自身の言語で書かれたコンパイラをコンパイルすること」はbootstrappingと呼ばれる。bootstrappingコンパイラをもつ言語は多くある。Rust以外のbootstrappingのプロセスについては何も知らないが、まあ共通するところは多いだろうと推測できる。

典型的なbootstrappingはいくつかのstageに分かれて段階的に行われる。以下ではstageごとにRustコンパイラのbootstrappingプロセスを見ていく。

Stage 0

RustのbootstrappingはPythonスクリプトを実行することで始まる。(だからこの時点ではPythonを実行できる環境でさえあれば良い。) このPythonスクリプトの主目的は2つ。

  1. 既存のRustコンパイラをダウンロードする。
  2. rustbuildをビルドして実行する。

順に見ていく。

まず現betaのRustコンパイラと関連ライブラリがバイナリの形でダウンロードされる。(betaはchannelの1つ。stableとかnightlyとかのアレ。) これがstage0で使うコンパイラとなる。つまりソースコードがビルドされる以前には、最新よりも少しだけ前のバージョンのコンパイラを使う。

次にこのstage0コンパイラを使ってrustbuildをビルドする。rustbuildとはRustで書かれたRustコンパイラのビルドシステムで、Rustコンパイラと同じリポジトリ内にある。ビルドが完了したらrustbuildが実行される。ここでやっとbootstrappingを実行する主体がPythonからRustに移る。

というわけで以下はrustbuildによってbootstrappingが進行する。

まずrustbuildはstage0コンパイラを使ってstd (標準ライブラリ) をビルドし、次にrustcをビルドする。rustcをビルドする際には、ビルドされたばかりのstdがstage0 rustcと共に使われる。

Rustコンパイラのビルド過程において、stage Nでstage Nコンパイラにビルドされたものたちはstage N artifactsと呼ばれる。つまりたった今ビルドしたstdやrustcはstage0 artifactsだ。

Stage 1

stage1の最初には、stage0でビルドされたstdやrustcといったstage0 artifactsがstage1用のディレクトリにコピーされる。これがstage1コンパイラとなる。stage0 artifactsを組み合わせてstage1コンパイラを形成するのである。このプロセスはupliftと呼ばれる。

あとはstage0と同様で、このstage1コンパイラを使って、stdやrustcを再度ソースコードからビルドする。

Stage 2

stage1と同様、stage1でビルドされたstdやrustcといったstage1 artifactsがupliftされてstage2コンパイラとなる。

通常、rustupなどによってユーザーのコンピュータにインストールされるRustコンパイラはこのstage2コンパイラのこと。ここにRustコンパイラのビルドが完了する。

stage2コンパイラはstage1コンパイラと理論上同じ機能をもつが、些細な部分で異なる。stage1コンパイラのビルドには、最新のソースコードから成るコンパイラではなくダウンロードしてきたコンパイラが使われる。このためABIレベルでの違いが発生し得る。(同じ理由で、クロスコンパイルする場合はstdはstage2で再ビルドされる。が、ここらへんは頭の中だけで考えるのはそれなりに難しい。)

実際のビルドコマンドの処理をざっと追ってみる

ここまででRustコンパイラのビルドプロセスの概要を一通り見ることができた。次は実際に使われるビルドコマンドを例にとって、それがどういうプロセスを経ているのか簡単に見ていく。

Rustコンパイラのソースコードをいじる際には./x.py build library/stdというコマンドでビルドすることが推奨されている。よく打つことになるこのコマンドを例にとる。

(NOTE: これを書いている時点より少し前は./x.py build -i library/stdが推奨されていたコマンドだったが、./x.py build library変更されたっぽい。が、ここでは./x.py build library/stdの挙動を見ていくことにする。)

前提的な話になるが、./x.pyには--stageというオプションがある。これはbootstrappingのstageを制御するオプションで、--stage Nと指定するとstage Nのコンパイラ (rustc) の実行を指定できる。./x.py buildコマンドでは--stage 1がデフォルトとなっている。したがって./x.py build library/stdは「stage1のコンパイラを使ってstdをビルドする」ことと説明できる。そこまでのプロセスを見ていく。

./x.py build library/stdというコマンドの通り、./x.pyというファイルの実行から始まる。これが上述したPythonスクリプトである。このファイルはただsrc/bootstrap/bootstrap.pyのmain関数をcallするだけ。
src/bootstrap/bootstrap.pyは既存のRustコンパイラ (stage0コンパイラ) をダウンロードした後、rustbuildをビルドして実行する。

ここからrustbuildの処理に移り、まずstage0コンパイラでstdをビルドする。このビルドされたstdとstage0 rustcでrustcを新たにビルドする。ここまででstage0が終わり。

stage1に移り、stage0でビルドしたstage0 artifactsをupliftしてstage1コンパイラができる。このstage1コンパイラでstdをビルドする。

これで./x.py build library/stdコマンドは終了する。見てきたようにstage1コンパイラとそれでビルドしたstdを得る。このように通常の開発フローにおいてはstage2コンパイラまではビルドしないでstage1までとすることが多い。

おまけ的に: 最古バージョンのRustのビルドは?

少し前のバージョンのRustコンパイラのバイナリで現バージョンのソースコードをコンパイルしていくのはわかった。しかし最古のバージョンのRustコンパイラはどうやってビルドしたのだろう。その段階ではRustコンパイラのバイナリも存在しないはず。

bootstrappingする言語において、最古のコンパイラは別言語で書かれている。Rustの場合はOCamlで書かれていたらしい。OCamlのソースコードはここに。(かなり小さくてすごい。)

おわりに

自分の言語で書かれたコンパイラをコンパイルするのは面白いが大変。
rustbuildの挙動をソースコードレベルで追っていきたい。別の記事で書く予定。

間違っている部分があれば指摘していただけると助かります。

参照

Discussion