Rustコンパイラ自体のビルドプロセスを理解する
この記事では、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つ。
- 既存のRustコンパイラをダウンロードする。
- 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の挙動をソースコードレベルで追っていきたい。別の記事で書く予定。
間違っている部分があれば指摘していただけると助かります。
参照
-
rustc-dev-guide: Bootstrapping
(この記事の内容はほぼ全てここに書いてある。) - Wikipedia: Bootstrapping (compilers)
Discussion