WasmLinux: ビルドシステムの設計
... これ絶妙に難しいな。。
ここまでで、必要なパーツの検証は一通り完了した。つまり、普通のLinuxプログラムを(Wasmで表現できる範囲のプログラムであれば)パッチなしで cc
に掛けてリンク → 実行できるようになった。
今は全部のビルド過程をシェルスクリプトを叩くことで1ステップづつ実行しているが、さすがにHello, World以上のユースケースをこの体制でビルドしていくのは無理があるので、そろそろ真面目なビルドシステムを作っていくことにする。
Overview
一旦、WasmLinuxシステムは 巨大な1つのexecutable としてビルドされるものとする。つまり、 busybox
とか dropbear
のような個々のプログラムも一旦は .a にして1つのexecutableに纏める。もちろん、将来的には .dll なり .so にできるようにしたいが。。
実際のビルドは7フェーズに分けられる。
ビルド成果物の配布ディレクトリ
個々のフェーズは、ビルド成果物としてディレクトリを構築する。
-
prefix
ディレクトリは、libcやLinuxカーネルのヘッダ、およびリンク時に使用する libc.a を含んでいる。 -
host
ディレクトリはWebAssembly向けのホストツールを含んでいる。これらはWindows用、Cygwin用、Linux用、... と各ホストツールで別々のものが必要になる -
kmdist
ディレクトリはLinuxカーネルのWebAssemblyモジュールおよびwasm2c変換されたCソースコードを含んでいる。 -
umdist
ディレクトリはシステムに含まれるLinuxユーザランドバイナリのWebAssemblyモジュールおよびwasm2c変換されたCソースコードを含んでいる。 -
rtdist
ディレクトリは、kmdistやumdistのコードを実行するのに必要なwasm2cランタイムのCソースコードを含んでいる。
最終的なシステムイメージを作成するPhase6では、 kmdist
umdist
rtdist
の各ディレクトリの内容を使用してシステムイメージを作成する。ここで host
や prefix
が必要ないのがミソで、ホストシステムの開発環境とWasmLinuxの開発環境は完全に分離できる。
Phase0: ホストツール(Clang, wasm2c)のビルド
WasmLinuxは超最新のClang(LLVM-18ベースのもの)が必須なので、ホストツールはその辺で配られていない。というわけで、Phase0としてホストツールのビルドが必要になる。
ビルド成果物: clang
などLLVMツールおよびwabt (将来的にはBinaryenも必要)、 wasm2c-rt (host)
wasm2cしたコードを実行するにはランタイムをリンクする必要がある。このランタイムはソースコード形式で提供する。
Phase1: Linux(LKL)のビルド
Linux自体はホストツールさえ存在すればビルドできる。LinuxをビルドするためにはCコンパイラの他に sed
等も必要だが、それらはシステムに存在すると仮定する。WindowsではCygwinが必要になり、macOSでは大文字小文字を区別するファイルシステムにチェックアウトする必要がある。
ビルド成果物: vmlinux.a
(kmdist) および カーネルヘッダ(prefix)
ここでビルドされる vmlinux.a
やカーネルヘッダはどこでビルドしても同じものができるので、非対応OSではスキップして他所から拾ってくることができる。
Phase2: libc(Musl)のビルド
MuslのビルドにはLinuxのカーネルヘッダが必要となる。将来的には、ココにcompiler-rtも含まれることになるだろう。
ビルド成果物: libc.a
および libcヘッダ(prefix)
ここのビルド成果物も、どこでビルドしても同じものができるので、スキップできる。
Phase3: カーネルイメージのビルド
vmlinux.a
はカーネルを構成する関数のみが含まれていて、実際のリンクは実施されていない。実際のリンクのためにはsyscallインターフェースのためのstub等も組み合わせる必要がある。
ビルド成果物: カーネルのWebAssemblyモジュール(kmdist)
Phase4: ユーザーランドのビルド
Phase2の成果物(prefix)を使用して各種ユーザランドバイナリをビルドする。
ビルド成果物: 各種ユーザランドのWebAssemblyモジュール(umdist)
Phase5: モジュールの変換
ここで、後述するモジュールIDをユーザーランドの各WebAssemblyモジュールにアサインする。
また、続くPhase6で一切の外部ツールに依存せずに済むように、各WebAssemblyモジュールに wasm2c
を掛けるのを独立したフェーズにしておく。
ビルド成果物: wasm2cされたCソースコード (kmdist + umdist) および wasm2cのランタイム (rtdist)
将来的にwasm2c以外のWebAssemblyランタイムをサポートした場合で、ランタイム毎の事前変換が別途必要な場合はこのフェーズで吸収する。
Phase6: システムイメージの作成
wasm2cされたコードやランタイムをビルドし、executableを出力する。
ビルド成果物: runner.exe
モジュールID
WasmLinux的な意味のユーザーランドexecutableはモジュールIDで区別される。
導出
モジュールIDはWebAssemblyモジュールのSHA256を使用する。今のところ正規化アルゴリズムは設定しない。(例えば、build-idを埋め込んでそれを使うとか、適当な順番にセクションを並びかえてから導出するといったルールは考えられる。)
"モジュールID化"
WasmLinuxのファイルシステム上にexecutableを置いといても仕方ないので、モジュールIDを書いた以下のようなファイルに置換する。
#!/usr/bin/env wasmlinux-id-exec
aabbccdd...
最初のshebang行は、将来Linuxシステム上で直接実行できるようになることを期待してのもの。
モジュールID化された元のWebAssemblyモジュールは以下のような位置に移動される:
/wasmlinux/modcache/aa/bb/ccdd.../base.wasm
(symlinkはsymlinkのまま残され、変換されない。)
friendly-unique name
モジュールIDはwasm2cでの変換の際にも使用されるが、流石にsha256がbacktrace等に現われても何がなんだかということになるので、フレンドリ名をアサインする。ビルドIDおよびフレンドリ名はphase5でアサインされる。
フレンドリ名は以下の優先度で選ばれる:
- 他のモジュールと同じSHA256を持つ場合は、先に見つかった方のbasenameを採用する
- 他のexecutableと被っていない場合は元executableのbasename
- 他のexecutableと被っている場合は全てを
basename_<sha256の先頭4ケタ>
に変更 - それでも被った場合はエラー
kernel
はカーネルに使用されるため特別扱いする。 kernel
という名前のユーザーランドモジュールが存在した場合は kernel_<sha256の先頭4ケタ>
を常に使用する。
フレンドリ名とsha256の関係は.tsvファイルに記録され、umdistに同梱される。