.NET Core への MSBuild のポーティング


 

本記事は、マイクロソフト本社の The Visual Studio Blog の記事を抄訳したものです。
【元記事】 Node.js: From Zero to Bobble with Visual Studio Code 2016/2/16

 

今回は、.NET チームのソフトウェア エンジニアを務める Daniel Plaisted の記事をご紹介します。

.NET は今、大きな盛り上がりを見せています。.NET Core (英語) の登場により、オープン ソースとクロス プラットフォームへのシフトは急速に進んでいます。.NET Core は、アプリケーション ローカル、クロス プラットフォーム対応、完全なオープン ソースといった特徴を持つ最先端のアプリ開発用フレームワークです。しかし、.NET Core には .NET Framework から変更された点がいくつかあるため、.NET Core で動作するように既存の .NET Framework コードをポーティングする場合、ある程度の作業が必要になる場合があります。

先日のブログ記事「.NET Core へのポーティング (英語)」では、.NET Core へのポーティングに適したコードの種類についてご説明すると共に、.NET Core へのコード移植に関するアドバイス、方針、ツールをご紹介しました。今回は、私たちが MSBuild を .NET Core にポーティングした際の体験談をご紹介したいと思います。これは .NET Core へのコード移植を行っている方の参考になるのではないかと思います。また、ポーティング作業のどの分野を改善できるかも明らかになるでしょう。

MSBuild は、.NET と Visual Studio のビルド エンジンで、corefx (英語) (.NET Core ライブラリ)、coreclr (英語) (.NET Core ランタイム)、Roslyn (英語) (.NET コンパイラ プラットフォーム) といった多数のオープン ソースの .NET プロジェクトをビルドするために使用されています。これらのプロジェクトはいずれもオープン ソースかつクロス プラットフォーム対応であるため、Windows を使用しなくてもこうしたプロジェクトに対する変更、ビルド、貢献を可能にしたいとマイクロソフトは考えています。これを実現するために、昨年 3 月に MSBuild をオープン ソース化し、9 月には .NET Core (英語) を基盤としてクロス プラットフォームで動作するように MSBuild のポーティングに取り組んでいることを発表しました。MSBuild を .NET Core にポーティングする作業はほぼ完了し、現在はポーティングしたコードを OSS リポジトリのインフラストラクチャに統合する (英語) ための取り組みを進めているところです。

 

戦略

MSBuild は当初、2015 年 5 月に GitHub でオープン ソース化されました。最初はリポジトリの独立したブランチ (英語) において、Mono ランタイムを使用して Linux 上で実行できるようにポーティングされました。このブランチが、.NET Core へのポーティングの出発点となったのです。最終的にはこのブランチの変更を元のマスターブランチにマージし、MSBuild の .NET Core バージョンと完全な .NET Framework バージョンの両方を同じソース コードからコンパイルすることを計画しています。

.NET Core バージョンの MSBuild の目標は、完全な .NET Framework や Mono に依存しなくても、Windows、Linux、Mac OS で .NET Core プロジェクトをビルドできるようにすることです。完全な .NET Framework バージョンの MSBuild との完全な互換性を確保することや、デスクトップ .NET Framework をターゲットとするプロジェクトをビルドすることが目標ではありません。なぜなら、このようなプロジェクトをビルドするには、.NET Core では利用できない機能 (グローバル アセンブリ キャッシュなど) が必要になる場合があるためです。

.NET Core バージョンの MSBuild がデスクトップ .NET Framework バージョンとの完全な互換性を備えることはないものの、最終的にはコードベースをマージしたいと考えているため、私たちはコード内で .NET Core バージョンとデスクトップ バージョンが異なる部分に条件付きコンパイルを採用しました。AppDomain のサポートには FEATURE_APPDOMAIN (英語)、バイナリ シリアル化には FEATURE_BINARY_SERIALIZATION (英語) といったように、詳細な機能フラグ (英語) を使用したのです。これにより、NETCORE のような単一のコンパイル定数を使用した場合と比べて、.NET Core でコードの特定の部分が有効になっていない理由が明確になります。また、こうした機能の一部が .NET Core に追加された場合、.NET Core バージョンの MSBuild の対応するコードに立ち返って有効化することも容易になります。

 

ポーティング

進捗管理

最初に、.NET Core への MSBuild のポーティングに必要な作業の内容と作業量を把握するために、ApiPort ツール (英語) を使用して、MSBuild で使用されていて .NET Core ではサポートされていない API を割り出しました。その後は、ApiPort をポーティング作業の進捗管理に使用しました。

ApiPort は、コンパイル済みのマネージ アセンブリを分析するツールです。そのため、分析するにはコードを正常にコンパイルする必要があります。当然ながら、ポーティングが完了するまでは .NET Core 用にコンパイルすることはできないため、.NET Framework 用にコンパイルしたコードに対して ApiPort を実行しました。通常の .NET Framework ビルドの動作を変えることは避けたかったので、正常にコンパイルされたコードを得るために、.NET Framework をターゲットとしながら .NET Core と同じ機能フラグの構成を採用した新しいビルド構成を作成しました。

しかし、.NET Core 用にプロジェクトをコンパイルする作業の進捗管理には ApiPort を使用できないことが判明しました。プロジェクトを .NET Core に正常にポーティングしても、ApiPort によって報告される移植性のほとんどが 100% を下回っていたのです。その主な原因は、一部のケースで .NET Framework から MSBuild のコードベースに API をコピーしたことです。こうした API は、自分たちの手で実装を追加してプロジェクトで利用できるようにしたとしても、ApiPort による移植性の分析時に利用不可と報告されます。もう 1 つの原因は、ApiPort では常に特定のリフレクション API がサポート対象外として検出されることです (詳細は GitHub のバグ報告 (英語) を参照)。次の表に示すのは、.NET Core をサポートするための最低限の変更を行った MSBuild プロジェクトについて報告された移植性スコア (Initial score) と、.NET Core 用に正常にコンパイルできるようにプロジェクトをポーティングした後のスコア (Final score) です。

clip_image002

ApiPort には、他にも軽微な問題が 2 つありました。それは、(PInvoke による) ネイティブ API の呼び出しが分析されない (英語) ことと、API の使用回数ではなく (英語)、個別の API の数に基づいて移植性スコアが報告されることです。そのため、プロジェクト内で 1 回しか使用されていないサポート対象外の API の呼び出しを削除すると移植性スコアが上がりますが、サポート対象外の API の多数の呼び出しを削除しても、その API の使用箇所をすべて削除しない限りスコアは変わりません。

今回のケースでは、ApiPort によって報告される移植性スコアは誤って解釈されるおそれがありますが、このスコアの「正確性」を向上させることに必ずしも意味はないと私は考えています。コードをポーティングする作業の量や期間を魔法のように教えてくれるツールなど存在しません。ApiPort は指標を報告するだけなので、他のすべての指標と同様に、その本当の意味を理解していれば役立てることができます。

 

.NET Core 用のビルド

.NET Core へのポーティングにおける最初の作業は、.NET Core 用にコードをコンパイルできるようにビルド システムを準備することでした。当時、.NET Core を使用して設定不要でアプリをビルドして実行するための手段は DNX のみでした。DNX のプロジェクト システムは、MSBuild ほどの拡張性や柔軟性を備えていません。それがそもそも MSBuild を .NET Core にポーティングしようとしている理由の 1 つです。そのため、DNX は今回の作業に適していませんでした。

そこで、Eric St. John (英語) が .NET Core コンソール アプリケーションの概念実証が可能なサンプル プロジェクトを提供してくれました。このプロジェクトを参考にして、私たちは MSBuild プロジェクトを .NET Core 用にコンパイルするためのビルド構成 (英語) を準備することができました。現時点では、.NET Core をターゲットとしたツールは依然として開発中ですが、.NET Core をターゲットとした MSBuild プロジェクトを作成したい場合は、「.NET Core アプリとクラス ライブラリの作成方法 (英語)」のガイドに記載されている手順を実行してください。

 

.NET Core へのコードポーティング

.NET Core へのコード移植の主な作業は、.NET Core では利用できない API の使用箇所を削除することです。API の使用箇所を削除する必要があるコードを見つける手段として、私は主に Visual Studio のエラー一覧を活用しました。多くの場合、.NET Core で同じ機能を実現する代替 API が存在します。この例として一般的なのは、リフレクション、ファイル I/O、カルチャ、グローバリゼーションなどの各 API です。

.NET Core の MSBuild に適用できない API については、使用箇所を簡単に削除できました。この例としては、XAML 統合やグローバル アセンブリ キャッシュに含まれるアセンブリの解決があります。.NET Core で API が利用できない場合は、必要に応じて .NET Framework から API の実装をコピーし、MSBuild のコードベースに追加して .NET Core 用にコンパイルできるようにポーティングしました。この作業を行ったのは、XmlTextWriter とその依存関係、Environment.GetSpecialFolder、Type.InvokeMember です。今後、これらの API を一般的に適用可能な機能として .NET Core に追加すべきかどうかを .NET Framework チームと議論する予定です (英語)

API の使用箇所を削除するには、機能を再実装するか完全に削除します。ただし、大規模な再実装を行うと動作が変わるおそれがあります。実際に、私は当初 Type.InvokeMember の機能を再実装しました。その時点では最も簡単な解決策だと思ったからです。しかし、作成したコードは期待する動作とは異なったため、結局は2 回 (英語) も .NET Framework のコードに置き換えることになりました (英語)

現在、.NET Core 向けの MSBuild ではマルチプロセスのビルドはサポートされなくなっています。.NET Framework の現在の実装では、.NET Core ではサポートされていないバイナリ シリアル化によってプロセス間の通信を行います。この機能を .NET Core でサポートする (英語) には、通信用に別のシリアル化メカニズムを採用して再実装する必要があります (.NET Core へのバイナリ シリアル化の追加は予定していません。さまざまなランタイムやオペレーティングシステムへの対応が不十分であるためです)。

MSBuild のコア エンジンを .NET Core にポーティングするために行われた変更については、プル リクエスト #152 (英語)#156 (英語)#158 (英語)#159 (英語) をご覧ください。

開発者の皆様が私たちと同じ方法で .NET Framework のコードを .NET Core プロジェクトにコピーしようとすると、意図せずライセンスに違反してしまう可能性があります。.NET Framework のソース コードを参照する場合には、referencesource.microsoft.com (英語) の Web サイトにアクセスするのが最も簡単な方法ですが、掲載されているコードは「参照目的」でのみライセンスが付与されています。このライセンスは制限が非常に厳しく、コードをプロジェクトにコピーすることが許可されていません。このコードの大部分は、https://github.com/microsoft/referencesource (英語)corefx (英語) リポジトリ、coreclr (英語) リポジトリから MIT ライセンスで利用できます。最初に必要なコードをリファレンス ソースのサイトで見つけたら、ライセンスに違反しないように注意して、いずれかの MIT ライセンスのリポジトリで対応するコード (利用可能な場合) を検索し、そこからダウンロードする必要があります。

 

ポーティングに役立つツール

.NET Core へのコードのポーティングに役立つツールは多数あります。ApiPort では、.NET アセンブリをスキャンし、コードで使用されていて .NET Core ではサポートされていない API と、各 API の推奨される代替 API (ある場合) の一覧を Excel スプレッドシートに出力できます。しかし、ポーティング作業を進めるうちに、Excel シートを参照して API を確認するよりも、.NET チームの内部ツールである API Catalog を使用する方が便利なことに気付きました。その理由は、API が属しているコントラクトが表示されることと、優れた API 検索機能を備えていることです。さらに、ポーティング作業の大半が済んだ後で、同様の機能を備えた dotnetstatus サイト (英語) が公開されていることを発見しました。

ポーティング時にこれらのツールを使用する主な目的は、API がサポートされているかどうか、サポートされていない場合は推奨される代替 API があるかどうかをすばやく確認することです。API がサポートされている場合は、どの NuGet パッケージを参照すればよいかわかるように、API が含まれるコントラクトをツールに表示する必要があります。上記のツールの中で、API が含まれているコントラクトが表示されるのは非公開の API Catalog のみです。また、API が含まれる NuGet パッケージを検索できるパッケージの逆引き検索用 (英語) Web サイトも存在します。

.NET Core へのポーティング時に API を調べるための主な手段として dotnetstatus サイト (英語) を使用できるようにするために、以下の機能強化を提案します。

  • 検索機能の強化 (#19 (英語))
    • 部分一致の検索結果を返す (例: 「GetCulture」で検索すると「GetCultureInfo」メソッドを含む結果が返される)
    • 検索結果で型メンバーに加えて型も返す
    • 検索結果を別のページに表示する (現時点では、検索結果は検索ボックスのオートコンプリート項目としてのみ表示される)
  • API が含まれるコントラクトを表示する (#20 (英語))

推奨される代替 API や、特定の API が .NET Core で利用できない理由を簡単に調べることができれば、「必然性がないのにその API が .NET Core から削除された」、「.NET Core へのコード移植は難しい」といった誤解を減らすことができます。

API によっては、推奨される代替 API が表示されません。このような場合に、API の推奨される代替 API に関する情報や、API がサポートされない理由に関する情報をスムーズに追加できるプロセスを用意することが重要です。現在、推奨される代替 API の情報については GitHub でホストされており (英語)、だれでもプル リクエストを送信して、新しい推奨 API を追加したり既存の推奨 API を更新したりすることができます。

この API に関する情報をコードの編集中に Visual Studio に直接表示すれば、さらに利便性が向上します。表示方法としては、コンパイルエラーが表示されるエラー一覧か、コードに表示される「電球」アイコンを使用することが考えられます。また、一部の単純かつ一般的なコード変更を自動的に行うリファクタリングツールを提供してもよいでしょう。多数のリフレクション API で GetTypeInfo() の呼び出しを追加する必要があることや、Thread.CurrentCultureCultureInfo.CurrentCulture に置き換える必要があることは覚えておくべきですが、こうした機械的な変更を何百回も行っていると、作業を支援するツールを使用したいと考えるようになります (英語)

 

テスト

当初、MSBuild のテストは MSTest を使用して作成されていました。最初に行われた Mono と Linux へのポーティングの一環として、これらのテストは NUnit に変換されました。これは、MSTest がクロス プラットフォームに対応していなかったためです。しかし、当時 .NET Core をサポートしていたテスト フレームワークは xUnit のみでした。

私たちは、まずメインのコードベースのテストを xUnit に変換してから、それらの変更をクロス プラットフォームのコードにマージすることにしました。これにより、コードベースの同期を維持し、将来的にマージすることが容易になると考えられます。この作業には、XUnitConverter (英語) ツールを使用しました。このツールは、Test 属性を Fact 属性に変更し、名前空間を更新して、Assert 呼び出しの大部分を xUnit の API に変換します。

その後、これらの変更をクロス プラットフォームのコードベースにマージしました。クロス プラットフォームのコードベースのテストは NUnit に変換していたため、大量のマージ競合が発生しました。そこで、Rainer (英語) がこうした競合を自動的に正しく解決するためのツールを作成してくれました。

テストを xUnit と .NET Core に変換してみると、多数のエラーが発生しました。こうしたエラーの大部分は、.NET Core 向けの MSBuild に含めなかった機能のうち、テストの対象になっていたか、テストが間接的に依存していたものが原因でした。エラーが発生している原因を解明し、.NET Core 用に機能を修正すべきか無効にすべきかを判断するためには多くの調査が必要でした。短期間でエラーのない状態にするために、私たちは関連するテストやエラーの原因が同じであるテストをグループ化し、グループ全体を無効にしたうえで、さらに徹底的に調査するためにイシューを作成しました。

 

NuGet

.NET Core は、複数の NuGet パッケージとして提供されるモジュール式のフレームワークです。私たちにとって、パッケージ ベースのアプローチに移行することは非常に困難でした。その理由は、多くの要因が重なったことです。まず、.NET Core をターゲットとした開発をサポートする NuGet の機能は非常に新しいものでした。また、この機能は Visual Studio Tools for Windows 10 で提供されており、既定では最新のプラットフォームをターゲットとする UWP プロジェクトとポータブル クラス ライブラリ プロジェクトに対してのみ有効になっていました。つまり、この機能はチームにとって新しく不慣れなものであり、一部の機能はドキュメント化されていなかったうえに、ツールで直接サポートされていないプロジェクトで利用していたのです。その結果、複数のバグに行き当たり (その後修正されました)、多くの場合、NuGet のエラー メッセージや出力では問題の内容や発生した理由を判断することができませんでした。

ポーティングを始めた時点では、.NET Core でコンソール アプリケーションが提供されていなかったため、.NET Core 用の NuGet クライアントは存在しませんでした。そこで、Windows では完全な .NET Framework 上で実行される NuGet.exe を引き続き使用しました。Linux では、同じクライアントを Mono 上で実行しようとしました。このクライアントは、Mono での実行を目的として設計されていないことを考えると、ある程度は良好に機能しましたが、タイムアウトやパッケージのダウンロード失敗といった問題が発生しました。.NET Core では、それまでのプロジェクトの大半が参照していたよりもはるかに多くの NuGet パッケージを使用しているため、すべてのパッケージがキャッシュにダウンロードされるまでに NuGet restore コマンドを繰り返し実行する必要がありました。近いうちに .NET CLI (英語) の NuGet restore コマンドに切り替えれば、こういった問題は解消されると考えています。というのも、.NET CLI はクロス プラットフォーム対応の設計となっており、.NET Core で動作し、パッケージ復元の速度と信頼性が強化されているからです。

.NET Core 向けのビルドには、NuGet パッケージから適切なアセットを選択するための MSBuild タスク (ResolveNuGetPackageAssets) も必要です。これは Linux や .NET Core では利用できませんでした。Val (英語) は Linux と Mono にこのタスクを導入する 2 つのパッケージを見つけましたが、その動作は「本物の」タスクの動作と完全に一致するわけではなく、プロジェクトごとに 2 つのパッケージで異なるエラーが発生しました。

マネージ言語チームでは、私たちが利用している複数のタスクやターゲットを所有しているため、それらを NuGet パッケージとして公開してほしいと考えました。これらの項目の一部は、ターゲット ディレクトリのサブフォルダーに展開する必要がありますが、この方法は NuGet ではサポートされていませんでした。このシナリオについて NuGet チームと話し合ったところ、NuGet チームは次期リリースに向けて既に計画していた機能 (英語) を提案して私たちのニーズを満たしてくれました。

 

まとめ

この記事では、.NET Core へのポーティング作業の体験談をご紹介しました。皆様が興味を持つきっかけになったり、実際に .NET Core にコードをポーティングしている方にとってお役に立つ情報になれば嬉しいです。皆様の体験談もぜひお聞かせください。どのような課題に直面したか、どの .NET Framework の API が .NET Core で最も役立つと思うかなど、さまざまなコメントをお待ちしています。また、ポーティングを検討しているプロジェクトに対しては、必ず ApiPort を実行 (英語) してください。

今後も皆様からのご意見や ApiPort ツールから受け取ったレポート (英語) などを参考に、完全な .NET Framework の API の多くを .NET Core に追加する予定です。また、全体的なエクスペリエンスや .NET Core へのコード移植に役立つツールも引き続き強化してまいります。

ぜひ、.NET Core を皆様のポーティング作業にご活用ください!

Comments (0)