dotnetはどのように作られているのか

2017/03/21追記: この情報も大分古くなったので、現時点での最新情報は藤原さんによるこちらのエントリを参照すると良いでしょう。

Windows & Microsoft技術基礎Advent Calendar、25日目のエントリーです。最終日に相応しく、壮大な釣りタイトルを用意しましたが、ここでいう dotnet とは、.NET Coreにおけるコマンドラインツール dotnet のことです。

dnxに代わって最終的な.NET Coreの中心的な機能をコマンドラインで実現する予定であるこのコマンドは、.NET Coreの基礎を担う存在となるはずです。

そしてこれはオープンソースで公開されているのですから、自分でビルドすることもできるはずです。また、オープンソースディストリビューションオープンソースコンポーネントとして含まれるためには、その構成要素が本当に全てオープンソースで実現できているのか、確認する必要があります。

そういうわけで、今回は dotnet/cli リポジトリをチェックアウトしてビルドし、その内容を調べてみました。 https://github.com/dotnet/cli

はじめにお断りしておきますが、今回言及する一連のモジュールのビルドには、相応のリソース(ディスクスペース、ネットワーク)がかかります。実験するなら、家に引きこもって試す、大容量ストレージを用意する(札束で殴ってSSDを入手するか、HDDのI/Oでも良しとする忍耐心を身に付ける)等のノウハウを身につけてからが良いでしょう。

にわかの調べ物なので、適宜ツッコミ等お待ちしています。

dotnet/cli - dotnetのパッケージをビルドするツール

cli のREADME.mdの最初にはこう書いてあります:

This repo contains the source code for cross-platform .NET Core command line toolchain. It contains the implementation of each command, the native packages for various supported platforms as well as documentation.

このリポジトリには、コマンドラインツールチェインのソースコードが含まれている、と書いてあります。実際の役割は、(昔の).NET Framework SDK Tools、のサブセットのような感じです。実際のツールはほぼdotnetのみです。もっとも、この中には、C#/VBコンパイラーの呼び出しのためのdotnet-compileにはじまって、.NET Native化(dotnet-compile-native)、リソースのコンパイルdotnet-resgen)、配布パッケージ作成のためのdotnet-publishなど、さまざまな機能が実装されています。dotnetコマンドラインヘルプにresgenコマンド等は出てきませんが、dotnet resgenを実行すると、dotnet-resgenが呼び出されます。(実質的に単なるシェルスクリプトエイリアスっぽく見えますが、形式的には.NET Native化された別々のバイナリです)

実は、dotnet/cliリポジトリには、ツールの実体ソースはそれほど含まれていません。このリポジトリでビルドできるのは、インストールできるパッケージです。たとえば筆者の環境では、Ubuntu用の.debパッケージが出来上がります。WindowsならWixによるインストーラー パッケージが、OSXならproductbuildによる .pkg 形式のパッケージが、それぞれ出来るはずです。rpm系のパッケージビルダーはまだ無いようです。最近Microsoftと提携したRed Hatとか、コミュニティの誰かがcontributeすれば出来るのではないでしょうか。

では、dotnetパッケージの内容となる各種ツールはどのようにビルドしているかというと、実は重要な部分の多くは、ソースからのビルドを行っていません。代わりに、プライベートのnugetパッケージ リポジトリなどから、ビルド済みのバイナリをダウンロードしてきて、それをパッケージしています。実のところ、ビルドに必要なものを落としてくる方式は、.NET Core関係のリポジトリ群における標準的なものです。

dotnet/cliをチェックアウトして、build.shあるいはbuild.cmdを実行してみましょう。最後まで上手くいくと、パッケージがartifacts/packages/以下に作成されるはずです。

dotnetパッケージの内容

ではdotnetパッケージには何が含まれているのでしょうか。build.shを実行するとsudoパスワードを要求されて.debのパッケージまでインストールされますが(こわい!)、ここでインストールされたdotnetパッケージの内容を dpkg -L dotnet で見るとこうなります。 https://gist.github.com/atsushieno/78c2f5aef6477c3337a9

500行くらいあるのでgistにしましたが、内容はほぼ /usr/bin/dotnet*/usr/share/dotnet/* のみです。binの内容はこの通りただの /usr/share/dotnet/bin 以下へのシンボリックリンクなので、実質的に全てshare/dotnet以下に入っています。

ではこのディレクトリの中には何が入っているのでしょうか。少しずつグループに分けて見て行きたいと思います…が、無駄に長いので、次の節に進む前の「まとめ」だけ見ると良いと思います。

(1) /usr/share/dotnet/runtime/coreclr

ここには、coreclrの内容が含まれているようです。coreclrのソースは dotnet/coreclr にあります。

(2) /usr/share/dotnet/bin/corehost

以下、いくつかのファイルは、dotnet/cli 直下でビルドされ、dotnetの各コマンドの実装で使われています。このcorehostは、.NET Coreのホスティング環境で、配布パッケージに埋め込まれるものです。続くdotnet-run.dllは、dotnet runコマンドの実装です。

(3) /usr/share/dotnet/bin/Microsoft.Extensions.FileSystemGlobbing.dll

以下、いくつかのファイルは、dotnet/cli に含まれるツールの依存関係です。このdll自体は、githubのaspnet/FileSystem以下にあるモジュールで、dotnet-run.dllのソースで依存関係としてproject.jsonで指定されています。

(「いくつかのファイル」と書きましたが、FileSystemはdnxの唯一のsubmoduleでもあるので、もしかしたらこれだけが特別かもしれません。)

(4) /usr/share/dotnet/bin/dnx

このディレクトリには、ビルドスクリプトで直接ダウンロードされたdnxのパッケージが展開されます(ビルドスクリプトOSX/Linux用のshellとWindows用のpowershellで別々ですが、だいたい同じ処理内容になっていると思います)。dnxのソースはaspnet/dnxにあります。

(5) /usr/share/dotnet/bin/csi

csi。roslynに入っているC# interactiveです。dotnet-repl-csiを実装するために使われているのでしょう。roslynのソースはdotnet/roslynにあります。

(6) /usr/share/dotnet/bin/crossgen

crossgenは、ネイティブイメージの生成に使用されるクロスコンパイラーです。これは、.NET Nativeの実装…ではなく、従来のngenの実装(あるいはその一部)で、coreclrに含まれるツールです。

(7) /usr/share/dotnet/bin/libPortableRuntime.a

今度は(今度こそ).NET Nativeの一部です。.NET Nativeのソースはdotnet/corertにあります。他にもilc.exeなどcorert由来のものがしばらく続きます。

(8) /usr/share/dotnet/bin/dotnet-resgen.dll

この辺はdotnet/cliの一部で、dotnet-resgenを実装しています。

(9) /usr/share/dotnet/bin/csc.dll

ここからはだいたいroslynの一部です(たまにcliのバイナリも見られますが)。

(10) /usr/share/dotnet/bin/appdep

appdepというのは、実際にはdotnet-compile-nativeコマンドの機能のひとつである .NET Nativeコンパイラーの依存関係で、これ自体はnugetパッケージになっています。ソースとしてはcorertということになります。

パッケージ内容のまとめ

… 以上を総括すると、dotnet/cli の内容は、概ね次の各パッケージの内容を引っ張ってきていると言えます:

  • dotnet/coreclr
  • dotnet/corert
  • dotnet/roslyn
  • aspnet/dnx
  • aspnet/その他各種依存ライブラリ

dotnetのビルド

dotnetのビルドは、dotnet/cli/build.shを追いかければ分かると思いますが、3ステージに分かれています。 - stage0: stage 1 dotnetをビルドするためのツールをダウンロードします。主にscripts/compile.sh - stage1: dotnetをstage 0の(ダウンロードした)dotnetでビルドします。主にscripts/build/build-stage.sh - stage2: dotnetをstage 1のdotnetでビルドします。その後roslynにcrossgenをかけたり、.NET Nativeの依存関係(appdeps)をかけ、dotnet publishで最終的なパッケージ内容を構築します。

stage 1 buildを実行するbuild-stage.shの中で、各.NET Coreプロジェクトをビルドしているのが、このパッケージのビルドの主要な部分と言えるでしょう。

dotnetの依存関係

さて、繰り返しになりますが、dotnetのビルドでは、以下のモジュールが(直接 or 間接的に)バイナリダウンロードされているらしいことが、前述の通り明らかになりました。

  • dotnet/coreclr
  • dotnet/corert
  • dotnet/roslyn
  • aspnet/dnx
  • aspnet/その他各種依存ライブラリ

実際には、これらのモジュールが連鎖的に他のモジュールを取り込んでいる部分があります。わかりやすい例としては、dnxが参照するcorefxが挙げられるでしょう。

また、各依存モジュールが、再帰的に自分自身を参照している場合があります。たとえば、corertのビルド時には、cliが生成するパッケージ(dotnet)がダウンロードされて使用されます。

というわけで、他のモジュールがどうやってビルドされているのかも多少追いかけないと、dotnetがどのようにビルドされてその世界を構築しているのか、わからないはずです。

それぞれの分析については今後の課題としたいところですが、このcliモジュールのビルドと各モジュールのビルドの間で齟齬がないか、確認する目的で、一応それぞれをビルドしてみました。以下に、各関連リポジトリのビルド出力のファイルリストを列挙しておきます。

ここまでの考察、と補足的な分析

(1) dotnetはdnxを置き換えるために導入されたのではなかったの? 何でdnxが使われるどころかパッケージにまで含まれているの? という疑問が生じてきますが、わたしは、現状ではdotnet runコマンドの実装はdotnetコマンドでまだ(?)実現できず、最終的にdnvmに相当する機能などが整備されたらdotnetに置き換え可能になるのではないか、と想像しています。.NET Coreは現在RCですが、これは機能的にフリーズということであって、パッケージングの調整などはこれから行われるのではないか、最終バージョンに含まれるようになるかは分からないだろう、という感じで理解しています。

(2) パッケージの中にいくつかファイルの重複が見られますが(libcoreclr.soはいくつあるの?)、これは、それぞれのプログラムをdotnet publishで生成した結果であろうと思います。その意味では.debの内容を細かく分けて分析する意義はあまりなかったかもしれません(!)

同じことが、この cli 以外の各モジュールについても言えます。roslynのパッケージにはcoreclrやcorefxのファイルが含まれていますが、おそらく大半はdotnet publishでバンドルされたものであって、roslyn自身の実体は、csc, csi, vbc, Microsoft.CodeAnalysis.*.dllくらいでしょう。

(3) バイナリパッケージをダウンロードしてきてビルドするというのは、ビルドに無駄な時間がかからない、依存関係で破綻する可能性が少し低くなる、といったメリットはありますが、同時に、どこまで本当にオープンソースでビルドできるのか分からない、という問題もあります。たとえば、上記のリストからはroslynのUbuntuビルドにcsi.exeは含まれていない、ということが分かります(roslynのビルドは、WindowsではRoslyn.slnを、それ以外の環境ではCrossPlatform.slnを使用するので、その辺りで齟齬があるのでしょう)。これはWindowsでビルドされたものが巡り巡ってバイナリとしてUbuntuに含まれている、ということでしょう。この辺りは解明・解決しておかないと、Linuxディストリビューションオープンソースのパッケージとして含まれることができなくなってしまう可能性があります。

dotnetはそのビルドの性質から、既存のdotnetバイナリ パッケージを前提としていますが、それはC#コンパイラとmscorlib.dllがC#で書かれているmonoなんかもそうですし、現在ならコンパイラツールチェイン全般に言えることだと思います。オープンソースのツールチェインによってビルドできるようなソースが存在することが自明であれば十分であり、卵と鶏問題を厳密に追及するのは、ライセンスの観点からは意味がなく、実質的には非生産的です。)

(4) coreclrのビルド出力を見ると分かりますが、この中にはcorefxのアセンブリは含まれていません。一方、cliのパッケージの内容としては、dotnet/runtime/coreclrディレクトリ以下にmscorlib以外のcorefxアセンブリ含まれています。これらはどこから来ているのでしょうか?

その答えはscripts以下で “runtime/coreclr” をgrepしたら、scripts/build/build-stage.shのみが引っかかったことを契機に分かりました。このスクリプトの中で、明示的にsrc/Microsoft.DotNet.Runtimeをビルドしているのですが、ここにはREADME.mdがあって、内容にはこうあります:

This is a utility project that brings the CoreCLR down and lets us publish it as though it were an app. Do NOT add any C# code to it! It is not designed to actually be compiled as an assembly, it's just used for its dependencies.

このプロジェクトがruntime/coreclrディレクトリを出力パスとしてビルドされると、ここにproject.jsonで記述された依存関係が展開されることになります。実のところ、dependenciesは次の1行のみです。

"dependencies": {
  "NETStandard.Library" : "1.0.0-rc2-23616"
},

この"NETStandard.Library"のパッケージ、実体はcorefxのstandard platformに対応する"netstandard"のことだろうと思うのですが、どのモジュールから生成されているのか、今回は発見できませんでした。corefx(少なくともmaster)には無さそうです(nuspecが見当たらない)…ここにあるべきだとは思うのですが。

(5) corefx自体は膨大なアセンブリの集積であり、dotnetでは全てをパッケージすることはありません。各アプリケーションに必要なものは、project.jsonなどに記述すれば、dotnetコマンドがnuget経由でダウンロードしてくることになるでしょう。

dotnet/coreclr and co.

さて、今回、先ほど言及した、ダウンロードされてくる.NET Coreモジュールの全てについて、厳密にビルドを追いかけるのは、ちょっと現実的ではありませんし(何しろ書こうと思い立って調べ始めたのが3日前くらいですし!)、今回はあとdnxについて軽く言及する程度にしておいて、他のモジュールについてはざっくりと書いておこうと思います。

coreclrは、Cで書かれたランタイムであり、元来このモジュールだけで全て完結していました。mscorlibのビルドは(cscを使って)全ての実装がビルドされていました。これが、昨今ではそのソースをcorefxに移転しているようです。mscorlibの実装はやや特殊であり、ランタイムと密結合している部分が多いので、coreclrの一部となっていたわけですが、いずれにしろcoreclrとcorefxは切り離せない関係にあると言えるでしょう。

corefxのリポジトリには、coreclrをターゲットとする(けどdotnetで必須というわけではない)大量のアセンブリがあり、それらはnugetパッケージとしてビルドされます。実は、おそらくビルドの最終生成物はbinとpackagesの両方のディレクトリにあると思うのですが、今回はbinの内容だけを出して、packagesの内容は含めませんでした。ここにはcorefxのビルドに必要な依存関係もダウンロードされているようなので(たとえばdnx-monoやMSBuild.exeなど)、何がビルド出力なのか自明ではなかったためです。気になる人は一度ビルドしてみて下さい(ビルド作業は長大なものですが…)

corertは、ビルドに際してこのdotnetモジュールに依存していたりと、内容を精査してみないと分からない部分がありますが、ネイティブコンパイル機能自体は、ランタイムに必須の機能というほどでもないので、今回は調査対象外としました。個別に後で追いかける価値は、あるでしょう。

aspnet/dnx

dnxはgithub上ではdotnetではなくaspnet配下にあります(歴史的な事情によるものでしょうか)。

dnxのビルドにはKoreBuildとSakeというビルドシステムが使われます。これらはnugetパッケージとして取得されます。KoreBuildにはdnxが含まれており、つまりこのモジュールも鶏と卵の関係にあります。ちなみにnuget.exeもバイナリダウンロードされます。

KoreBuildは、おそらくSakeの応用として作られた、.NET Coreに特化したビルド構成ファイル群なのでしょう。KoreBuildのソースは、源流がどこにあるのかわたしには確認できませんでしたが、aspnet/Universe/KoreBuild-dotnetに一連のソースがあるようです(devブランチであり、masterにはありません)。

dnxに限らず、dnxを使うモジュールをビルドする際に気をつけておきたいのが、使用するdnvmのランタイムとバージョンです。Ubuntu環境であれば、現状coreclrではまだビルドできません。実のところ古いdnx-monoでも、dnxのビルドは失敗します。わたしは1.0.0-rc2-16348を使って何とかビルドできました。

ちなみに、dnxのビルドにはdnxが必要です。おそらくSakeがdnxで動作するのでしょう。dnxがビルドするプロジェクトは、その実行に必要なファイルを、dnx自身からコピーすることになります。ではdnxでビルドしたプロジェクトでは、どういうファイルが必要になるのでしょうか? それらは生成あるいはコピーされるのでしょうか? ここは未調査ですが、おそらくdotnetコマンドがMicrosoft.DotNet.Runtimeというダミープロジェクトをビルドすることで「ランタイム アセンブリ」のようなものを構築したのと、同じことが行われているのではないかと思います(つまり、project.jsonの内容に照らして、無いものはnugetからダウンロードしてくる、ということでしょう)。

いずれにしても、わたしの理解としては、dnxの使用は、ある程度過渡的なものであり、役割としても、ビルドの性格としても、概ねdotnetと重複するものであって、コレを別途追求する意義はあまりないのではないか、といったところです。

(Sake/KoreBuildが過渡的なものなのかどうかは、今は判断保留としておきます。)

総括など

ここまでざっと見てきたことで、dotnetツールが、全てをソースからフルビルドしているのではなく、関連モジュールの既成バイナリをnugetからダウンロードしてきて、それを展開するかたちで構成されていることが、そして、それぞれのビルドについて、適切にオープンなソースからビルドされたものが最終的に含まれているのかを調べる道筋が、何となく理解してもらえたのではないでしょうか。

今回調べたようなことを、CoreCLR読書会というグループで勉強会のようなかたちで共有しています。CoreCLR Book of the Runtimeの翻訳などもそこで行っています。興味のあるマニヤックな皆さんの参加をお待ちしています。そのうちまた開催すると思うので、twitterにアンテナでも張っていてください。