ブートストラップが行うこと
ブートストラップとは、コンパイラを使ってそのコンパイラ自身をコンパイルするプロセスです。 より正確には、古いコンパイラを使って、同じコンパイラの新しいバージョンをコンパイルすることを意味します。
これは「鶏が先か卵が先か」というパラドックスを生みます。最初のコンパイラはどこから来たのでしょうか?
それは別の言語で書かれていたに違いありません。
Rust の場合、それは OCaml で書かれていました。
しかし、それはずっと昔に放棄されており、現代のバージョンの rustc をビルドする
唯一の方法は、少しだけ古いバージョンを使うことです。
これこそが ./x.py の動作です。現在のベータリリースの
rustc をダウンロードし、それを使って新しいコンパイラをコンパイルします。
このドキュメントは、主にユーザー向けの情報を扱っていることに注意してください。 bootstrap の内部について読むには、bootstrap/README.md を参照してください。
ブートストラップのステージ
概要
- Stage 0: 事前にコンパイルされたコンパイラと標準ライブラリ
- Stage 1: 現在のコードから、以前のコンパイラによって作られたもの
- Stage 2: 真に現在のコンパイラ
- Stage 3: 同一結果のテスト
rustc のコンパイルはステージごとに行われます。
以下は、RustConf 2022 での Jynn
Nelson の ブートストラップに関する講演 をもとにした図で、
その下に詳細な説明があります。
A、B、C、D は、ブートストラップのステージの順序を示しています。
青 のノードは
ダウンロードされ、黄色 の
ノードは stage0 コンパイラでビルドされ、緑 のノードは stage1 コンパイラでビルドされます。
graph TD
s0c["stage0 compiler (1.86.0-beta.1)"]:::downloaded -->|A| s0l("stage0 std (1.86.0-beta.1)"):::downloaded;
s0c & s0l --- stepb[ ]:::empty;
stepb -->|B| s0ca["stage0 compiler artifacts (1.87.0-dev)"]:::with-s0c;
s0ca -->|copy| s1c["stage1 compiler (1.87.0-dev)"]:::with-s0c;
s1c -->|C| s1l("stage1 std (1.87.0-dev)"):::with-s1c;
s1c & s1l --- stepd[ ]:::empty;
stepd -->|D| s1ca["stage1 compiler artifacts (1.87.0-dev)"]:::with-s1c;
s1ca -->|copy| s2c["stage2 compiler"]:::with-s1c;
classDef empty width:0px,height:0px;
classDef downloaded fill: lightblue;
classDef with-s0c fill: yellow;
classDef with-s1c fill: lightgreen;
Stage 0: 事前にコンパイルされたコンパイラ
stage0 コンパイラは、デフォルトではごく最近の beta rustc コンパイラと、それに
関連付けられた動的ライブラリであり、./x.py が自動的にダウンロードします。
(./x.py を設定して、stage0 を別のものに変更することもできます。)
事前コンパイル済みの stage0 コンパイラは、その後、事前コンパイル済みの stage0 std とともに
src/bootstrap と compiler/rustc をコンパイルするためだけに使われます。
stage1 コンパイラをビルドするために、事前コンパイル済みの stage0 コンパイラと std を使うことに注意してください。 したがって、ツリーから新しくビルドされた std を持つコンパイラを使うには、 stage2 コンパイラをビルドする必要があります。
ここでは 2 つの概念が関係しています。コンパイラ(その依存関係一式を含む)と、その
「ターゲット」または「オブジェクト」ライブラリ(std と rustc)です。
どちらもステージ化されていますが、段階をずらした形で行われます。
Stage 1: 現在のコードから、以前のコンパイラによって作られたもの
次に、rustc のソースコードは stage0 コンパイラでコンパイルされ、stage1 コンパイラを生成します。
Stage 2: 真に現在のコンパイラ
次に、ツリー内の std とともに stage1 コンパイラを使ってコンパイラを再ビルドし、stage2
コンパイラを生成します。
stage1 コンパイラ自体は、事前コンパイル済みの stage0 コンパイラと std によってビルドされたものであり、
したがって作業ディレクトリ内のソースによってビルドされたものではありません。
これは、stage0 コンパイラによって生成された ABI が、stage1 コンパイラによって
作られるはずだった ABI と一致しない可能性があることを意味し、その結果、動的ライブラリ、テスト、
および rustc_private を使うツールで問題が発生する可能性があります。
proc_macro クレートは、proc_macro::bridge と呼ばれる C FFI レイヤーによって
この問題を回避しており、stage1 で使えるようになっていることに注意してください。
stage2 コンパイラは、rustup およびその他すべてのインストール方法で配布されるものです。
しかし、ビルドには非常に長い時間がかかります。なぜなら、まず
古いコンパイラで新しいコンパイラをビルドし、次にそれを使って新しい
コンパイラをそれ自身でビルドする必要があるためです。
開発では、通常は --stage 1 フラグだけを使って物をビルドしたいはずです。
コンパイラのビルド を参照してください。
Stage 3: 同一結果のテスト
Stage 3 は任意です。
新しいコンパイラの健全性を確認するために、stage2 コンパイラでライブラリをビルドできます。
何かが壊れていない限り、結果は以前と同一であるはずです。
ステージのビルド
スクリプト ./x は、各サブコマンドについて、あなたが最も意図していそうなステージを選んでくれるようにします。
以下は、デフォルトのステージを持ついくつかの x コマンドです。
check:--stage 1clippy:--stage 1doc:--stage 1build:--stage 1test:--stage 1dist:--stage 2install:--stage 2bench:--stage 2
--stage N を明示的に渡すことで、いつでもステージを上書きできます。
ステージについての詳細は、以下を参照してください。
ブートストラップの複雑さ
ビルドシステムは現在のベータコンパイラを使って stage1
ブートストラップ用コンパイラをビルドするため、コンパイラのソースコードは、
一部の機能がベータに到達するまでそれらを使うことができません
(そうでなければベータコンパイラがそれらをサポートしていないためです)。
一方で、コンパイラ組み込み関数 や内部機能については、それらの
機能を使わなければなりません。
さらに、コンパイラは nightly 機能(#![feature(...)])を多用します。
この問題をどのように解決できるでしょうか?
使われている方法は 2 つあります。
- ビルドシステムは
stage0でビルドするときに--cfg bootstrapを設定するため、cfg(not(bootstrap))を使って、stage1でビルドされるときだけ機能を使うことができます。 この方法で--cfg bootstrapを設定することは、ちょうど安定化されたばかりの機能に使われます。 それらの機能はstage0でビルドされるときには#![feature(...)]が必要ですが、stage1では不要です。 - ビルドシステムは
RUSTC_BOOTSTRAP=1を設定します。 この特殊な変数は、Rust の 安定性の保証を破る ことを意味します。つまり、nightlyではないコンパイラで#![feature(...)]を使えるようにします。RUSTC_BOOTSTRAP=1の設定は、コンパイラをブートストラップする場合を除いて、決して使うべきではありません。
bootstrap のステージを理解する
概要
これは、個別の bootstrap ステージを詳細に見たものです。
./x が使う規約は次のとおりです。
--stage Nフラグは、stage N コンパイラ(stageN/rustc)を実行することを意味します。- “stage N artifact” とは、stage N コンパイラによって_生成_されるビルドアーティファクトです。
- stage N+1 コンパイラは、stage N アーティファクトから組み立てられます。このプロセスは _昇格_と呼ばれます。
ビルドアーティファクト
./x でビルドできるものはすべて_ビルドアーティファクト_です。
ビルドアーティファクトには、以下が含まれますが、これらに限定されません。
stage0-rustc/rustc-mainのようなバイナリstage0-sysroot/rustlib/libstd-6fae108520cf72fe.soのような共有オブジェクトstage0-sysroot/rustlib/libstd-6fae108520cf72fe.rlibのような rlib ファイルdoc/stdのような、rustdoc によって生成された HTML ファイル
例
./x test tests/uiは、stage1コンパイラをビルドし、その上でcompiletestを実行することを意味します。 コンパイラに取り組んでいる場合、通常はこれが使用したいテストコマンドです。./x test --stage 0 library/stdは、ソースからrustcをビルドせずに 標準ライブラリでテストを実行することを意味します(「stage0でビルドし、その後アーティファクトをテストする」)。 標準ライブラリに取り組んでいる場合、通常はこれが使用したいテストコマンドです。./x build --stage 0は、stage0rustcでビルドすることを意味します。./x doc --stage 1は、stage0rustdocを使用してドキュメントを生成することを意味します。
やってはいけないことの例
./x test --stage 0 tests/uiは有用ではありません。これは beta コンパイラでテストを実行し、ソースからrustcをビルドしません。 代わりにtest tests/uiを使用してください。 これはソースからstage1をビルドします。./x test --stage 0 compiler/rustcはコンパイラをビルドしますが、テストは実行しません。 これはcargo test -p rustcを実行していますが、cargoは Rust のテストを理解しません。 これを使用する必要はないはずです。代わりに(引数なしで)testを使用してください。./x build --stage 0 compiler/rustcはコンパイラをビルドしますが、libstdもlibcoreさえもビルドしません。 ほとんどの場合、代わりに./x build libraryを使用したいはずです。 これにより、lang item を定義する必要なくプログラムをコンパイルできるようになります。
ビルドと実行
要するに、stage 0 は stage0 コンパイラを使用して stage0 アーティファクトを作成し、
それらは後で stage1 コンパイラへ昇格されます。
0 以外の各ステージでは、2 つの主要なステップが実行されます。
stdが stage N コンパイラによってコンパイルされます。- その
stdが、stage N コンパイラによってビルドされたプログラムにリンクされます。これには stage N アーティファクト(stage N+1 コンパイラ)が含まれます。
stage N アーティファクトを、stage N コンパイラでビルドしている「単なる」
別のプログラムだと考えると、これはある程度直感的です。build --stage N compiler/rustc は、stage N アーティファクトを、stage N コンパイラによってビルドされた std にリンクしています。
ステージと std
ここでは、2 つの std ライブラリが関係していることに注意してください。
stageN/rustcに_リンク_されるライブラリ。これは stage N-1 によってビルドされたものです(stage N-1std)stageN/rustcで_プログラムをコンパイルするために使用される_ライブラリ。これは stage N によってビルドされたものです(stage Nstd)。
stage N std は、stage N コンパイラで有用な作業を行うためにはほぼ必須です。
これがないと、#![no_core] を使ったプログラムしかコンパイルできません。これはあまり役に立ちません!
これらが異なっている必要がある理由は、必ずしも
ABI 互換ではないからです。nightly には、beta には存在しない新しいレイアウト最適化、MIR への変更、または
Rust メタデータへのその他の変更が含まれている可能性があります。
ここで --keep-stage 1 library/std も関係してきます。
コンパイラへの変更のほとんどは実際には ABI を変更しないため、いったん
stage1 で std を生成すれば、おそらく別のコンパイラでそのまま再利用できます。
ABI が変更されていなければ、問題ありません。その std を再コンパイルする時間を費やす必要はありません。
--keep-stage フラグは単に、前回のコンパイルが問題ないと仮定するようビルドスクリプトに指示し、
それらのアーティファクトを適切な場所にコピーして、
cargo の呼び出しをスキップします。
rustc のクロスコンパイル
クロスコンパイルとは、別のアーキテクチャで実行されるコードをコンパイルするプロセスです。
たとえば、x86 マシンを使用して rustc の ARM 版をビルドしたい場合があります。
クロスコンパイルしている場合、stage2 std のビルドは異なります。
これは、./x が次のロジックを使用するためです。HOST と TARGET が
同じ場合、stage2 では stage1 std を再利用します!
これは妥当です。なぜなら、stage1
std は stage1 コンパイラ、つまり現在チェックアウトしているソース
コードを使用するコンパイラでコンパイルされたものだからです。
したがって、それは stage2/rustc がコンパイルする std と同一である(したがって
ABI 互換である)はずです。
しかし、クロスコンパイル時には、stage1 std はホスト上でしか実行されません。
そのため、stage2 コンパイラはターゲット向けに std を再コンパイルする必要があります。
(表で、stage2 が非ホストの std ターゲットのみをビルドすることを確認してください)。
‘sysroot’ とは何ですか?
cargo でプロジェクトをビルドする場合、依存関係のビルドアーティファクトは
通常 target/debug/deps に保存されます。
ここには cargo が
把握している依存関係だけが含まれます。特に、標準ライブラリは含まれていません。
std や proc_macro はどこから来るのでしょうか?
それらは sysroot から来ます。これは、コンパイラが実行時にビルドアーティファクトをロードする
複数のディレクトリのルートです。
ただし、sysroot は標準ライブラリを保存するだけではありません。実行時に
ロードする必要があるものはすべて含まれます。
これには以下が含まれますが、これらに限定されません。
- ライブラリ
libstd/libtest/libproc_macro。 rustc_privateを使用する場合の、コンパイラクレート自体。 ツリー内では、これらは常に存在します。ツリー外では、rustupでrustc-devをインストールする必要があります。- LLVM プロジェクト用の共有オブジェクトファイル
libLLVM.so。 ツリー内では、これはソースからビルドされるか CI からダウンロードされます。ツリー外では、rustupでllvm-tools-previewをインストールする必要があります。
ここまでに挙げたアーティファクトはすべて、コンパイラのランタイム依存関係です。
これらは rustc --print sysroot で確認できます。
$ ls $(rustc --print sysroot)/lib
libchalk_derive-0685d79833dc9b2b.so libstd-25c6acf8063a3802.so
libLLVM-11-rust-1.50.0-nightly.so libtest-57470d2aa8f7aa83.so
librustc_driver-4f0cc9f50e53f0ba.so libtracing_attributes-e4be92c35ab2a33b.so
librustc_macros-5f0ec4a119c6ac86.so rustlib
標準ライブラリにもランタイム依存関係があります!
これらは lib/ 直下ではなく、lib/rustlib/ にあります。
$ ls $(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/lib | head -n 5
libaddr2line-6c8e02b8fedc1e5f.rlib
libadler-9ef2480568df55af.rlib
liballoc-9c4002b5f79ba0e1.rlib
libcfg_if-512eb53291f6de7e.rlib
libcompiler_builtins-ef2408da76957905.rlib
ディレクトリ lib/rustlib/ には、hashbrown や cfg_if のようなライブラリが含まれています。これらは
標準ライブラリの公開 API には含まれませんが、その実装に使用されています。
また、lib/rustlib/ はリンカーの検索パスに含まれますが、
lib は検索パスに含まれることはありません。
-Z force-unstable-if-unmarked
lib/rustlib/ は検索パスに含まれるため、そこに
どのクレートを含めるかについて注意する必要があります。
特に、
標準ライブラリを除くすべてのクレートは -Z force-unstable-if-unmarked フラグ付きでビルドされます。これは、
それをロードするには #![feature(rustc_private)] を使用する必要があることを意味します(
常に利用可能な標準ライブラリとは対照的です)。
-Z force-unstable-if-unmarked フラグには、正しいクレートが unstable としてマークされていることを強制するのに役立つさまざまな目的があります。
これは主に、rustc と標準ライブラリが、staged_api を使用していない crates.io 上の任意のクレートへリンクできるようにするために導入されました。
rustc もこのフラグに依存しており、各クレートを慎重に unstable としてマークする必要がないように、すべてのクレートを rustc_private feature 付きの unstable としてマークします。
このフラグは、bootstrap スクリプトによって rustc 全体と標準ライブラリ全体に自動的に適用されます。
これは、コンパイラとそのすべての依存関係が、すべてのユーザーに対して sysroot 内で提供されるため必要です。
このフラグには次の効果があります。
- クレート自体が
stableまたはunstableとしてマークされていない場合、そのクレートをrustc_privatefeature 付きの “unstable” としてマークします。 - これらのクレートが、属性を必要とせずに他の強制的に unstable にされたクレートへアクセスできるようにします。
通常、クレートが他の
unstableクレートを使用するには、#![feature(rustc_private)]属性が必要です。 しかし、そうすると crates.io のクレートが自身の依存関係へアクセスできなくなります。なぜなら、そのクレートにはfeature(rustc_private)属性がない一方で、すべて が-Z force-unstable-if-unmarked付きでコンパイルされるためです。
-Z force-unstable-if-unmarked を使用しないコードでは、これらの強制的に unstable にされたクレートへアクセスするために、#![feature(rustc_private)] クレート属性を含める必要があります。
これは、Miri や clippy など、rustc 自体にリンクするものに必要です。
sysroot についての詳細な議論は次で確認できます。
sysrootから読み込まれる依存関係にextern crateを使用する理由を説明している rustdoc PR- Zulip での sysroot に関する議論
- rustdoc をツリー外でビルドすることに関する議論
bootstrap によって呼び出されるコマンドへフラグを渡す
便利なことに、./x では bootstrapping 時にステージ固有のフラグを rustc と cargo に渡せます。
RUSTFLAGS_BOOTSTRAP 環境変数は、bootstrap ステージ(stage0)に RUSTFLAGS として渡され、RUSTFLAGS_NOT_BOOTSTRAP は後続ステージ用のアーティファクトをビルドするときに渡されます。
RUSTFLAGS も機能しますが、bootstrap 自体のビルドにも影響するため、これを使いたいことはまれです。
最後に、MAGIC_EXTRA_RUSTFLAGS は cargo のキャッシュを迂回して、すべての依存関係を再コンパイルせずに rustc へフラグを渡します。
RUSTDOCFLAGS、RUSTDOCFLAGS_BOOTSTRAP、RUSTDOCFLAGS_NOT_BOOTSTRAPはRUSTFLAGSと同様ですが、rustdoc用です。CARGOFLAGSは cargo 自体に引数を渡します(例:--timings)。CARGOFLAGS_BOOTSTRAPとCARGOFLAGS_NOT_BOOTSTRAPはRUSTFLAGS_BOOTSTRAPと同様に機能します。--test-argsは引数をテストランナーへ渡します。tests/uiの場合、これはcompiletestです。 ユニットテストと doc test の場合、これはlibtestランナーです。
ほとんどのテストランナーは --help を受け付けるため、ランナーが受け付けるオプションを調べるために使用できます。
環境変数
bootstrapping 中には、多数のコンパイラ内部用の環境変数が使用されます。
rustc の中間バージョンを実行しようとしている場合、これらの環境変数の一部を手動で設定する必要があることがあります。
そうしないと、次のようなエラーが発生します。
thread 'main' panicked at 'RUSTC_STAGE was not set: NotPresent', library/core/src/result.rs:1165:5
./stageN/bin/rustc が環境変数に関するエラーを出す場合、それは通常、何かがかなりおかしいことを意味します。たとえば、rustc や std、または環境変数に依存する何かをコンパイルしようとしている場合などです。
そのような状況で本当に rustc を呼び出す必要があるというまれなケースでは、x コマンドに -vvv を追加することで、bootstrap shim にすべての env 変数を表示させることができます。
最後に、bootstrap は cc-rs crate を利用しています。この crate には、環境変数を通じて C コンパイラと C フラグを構成するための独自の方法があります。
ビルドコマンドの stdout の明確化
この部分では、実際の動作におけるビルドコマンドの stdout を調査します
(上のトピックと似ていますが、より詳細で完全なドキュメントです)。
x build --dry-run コマンドを実行すると、ビルド出力は次のようになります。
Building stage0 library artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
Copying stage0 library from stage0 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu / x86_64-unknown-linux-gnu)
Building stage0 compiler artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
Copying stage0 rustc from stage0 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu / x86_64-unknown-linux-gnu)
Assembling stage1 compiler (x86_64-unknown-linux-gnu)
Building stage1 library artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
Copying stage1 library from stage1 (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu / x86_64-unknown-linux-gnu)
Building stage1 tool rust-analyzer-proc-macro-srv (x86_64-unknown-linux-gnu)
Building rustdoc for stage1 (x86_64-unknown-linux-gnu)
stage0 の {std,compiler} アーティファクトのビルド
これらのステップでは、提供された(通常はダウンロードされた)コンパイラを使用して、ローカルの Rust ソースを、使用可能なライブラリへコンパイルします。
stage0 {std,rustc} のコピー
これは、ライブラリとコンパイラのアーティファクトを cargo から
stage0-sysroot/lib/rustlib/{target-triple}/lib へコピーします。
stage1 コンパイラの組み立て
これは、「stage0 の … アーティファクトのビルド」でビルドしたライブラリを、stage1 コンパイラの lib/ ディレクトリへコピーします。
これらは、コンパイラ自体が実行時に使用するホストライブラリです。
これらは、新しいコンパイラが生成するアーティファクトでは実際には使用されません。
このステップでは、生成した rustc と rustdoc のバイナリも build/$HOST/stage/bin へコピーします。
stage1/bin/rustc は、stage0(事前コンパイル済み)コンパイラと std でビルドされた、完全に機能するコンパイラです。
ツリー内のコンパイラと std を使って完全にソースからビルドされたコンパイラを使用するには、stage2 コンパイラをビルドする必要があります。これは stage1(ツリー内)コンパイラと std を使ってコンパイルされます。