クロスプラットフォームパッケージ作成を助けるMSBuildのSDK紹介


はじめに

現在のMSBuild(15.x)にはMSBuild Project SDKというものが追加されている。
MSBuild Project SDKとは、csprojでいう所の<Project Sdk="Microsoft.Net.Sdk">みたいな部分の事である。
大体はSDK付属のものを使用することになると思うが、NuGetで公開することもできる(15.6周辺で追加された?)。

今回はサードパーティー製のもので、役立ちそうなパッケージを紹介してみる。

MSBuild SDKの詳しい解説はdocs公式ページを参照

MSBuild.Sdk.Extras

Microsoft.Net.Sdkではカバーしきれていない、マルチプラットフォーム対応なnugetパッケージを作成するためのもの。

リファレンスアセンブリの作成

dotnet coreでランタイム(winとかlinuxとか)ごとの実装を持っているパッケージを作りたい場合(Windowsではwinhttp、linuxではlibcurlを使う等)、開発時は参照だけ(仮にALib.Ref)しておいて、後で実装DLL(仮にALib.Impl)に差し替えるというBait-and-Switchという方式のパッケージを作成するという方法がある。
この時、ALib.RefとALib.Implは別プロジェクトで作り、後で一つのパッケージに纏める必要があるが、この場合各プロジェクトの成果物を纏めるための諸々の設定を行う必要がある。
これは少々面倒な手順を踏む必要があるが、MSBuild.Sdk.Extrasはこの作業を補助してくれる。

まずは最低限必要なこととして、ALib.RefとALib.Implという二つのプロジェクトを作成する。
そして、それぞれのプロジェクトのcsprojを以下のように編集する

ALib.Ref.csproj
<!-- SDKの指定。Microsoft.Net.Sdkとの重複指定は不可 -->
<!-- NuGetから取得するので、バージョン番号を明示的に指定するか、global.jsonでバージョン指定する必要がある -->
<Project Sdk="MSBuild.Sdk.Extras/1.6.65">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
</Project>
ALib.Impl.csproj
<Project Sdk="MSBuild.Sdk.Extras/1.6.65">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0</TargetFrameworks>
  </PropertyGroup>
  <ItemGroup>
      <!-- リファレンスアセンブリとなるプロジェクトを指定 -->
      <ReferenceAssemblyProjectReference Include="../ALib.Ref/ALib.Ref.csproj"/>
  </ItemGroup>
</Project>

Impl側でReferenceAssemblyProjectReferenceを指定することで、指定したプロジェクトをリファレンスアセンブリとみなして、ALib.Implプロジェクトのpack時に一緒に同梱してくれるようになる。

ランタイム別のアセンブリの作成

現行のMicrosoft.Net.Sdkは、一つのnugetパッケージで複数のランタイムをサポートするものを作る、ということは直接サポートしておらず、これもまた纏めるための諸々の設定が必要になる。

ここでもMSBuild.Sdk.Extrasはこの作業を補助してくれる機能を提供している。

ALib.Implというプロジェクトで、以下のような記述を行う

ALib.Impl.csproj
<Project Sdk="MSBuild.Sdk.Extras/1.6.65">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0</TargetFrameworks>
    <!-- 実装したいランタイムを指定 -->
    <!-- RuntimeIdentifier(RID)については https://docs.microsoft.com/en-us/dotnet/core/rid-catalog を参照 -->
    <RuntimeIdentifiers>win;unix</RuntimeIdentifiers>
    <ExtrasBuildEachRuntimeIdentifier>true</ExtrasBuildEachRuntimeIdentifier>
  </PropertyGroup>
  <!-- Compileアイテムは重複で指定するとエラーになるので、一旦impl配下全てのcsファイルをコンパイルから除外する -->
  <ItemGroup>
    <Compile Remove="impl/**/*.cs"/>
  </ItemGroup>
  <ItemGroup>
    <!-- ランタイムごとに対応するネイティブバイナリがある場合 -->
    <None Include="native.so" PackagePath="runtimes/unix/native" Pack="true"/>
  </ItemGroup>
  <!-- win用の実装ファイルを作成 -->
  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'win'">
    <Compile Include="impl/win/*.cs"/>
  </ItemGroup>
  <!-- unix用の実装ファイルを作成 -->
  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'unix'">
    <Compile Include="impl/unix/*.cs"/>
  </ItemGroup>
</Project>

これにより、win用、unix用のそれぞれの実装が入ったバイナリが格納される。
ネイティブバイナリを追加したい場合は、ItemGroupに<None Include="native.dll" PackagePath="runtimes/[ランタイムID]/native" Pack="true"/>という項目を追加すれば良い。

これにより、dotnet packで生成されるパッケージに、それぞれのランタイム対応のライブラリが入る。
プリプロセッサで判断したい場合は、RIDに対応した名前がDefineConstantsに入るので、それを元に分岐すればOK。
RID="unix"ならば"UNIX"、RID="win"ならば"WIN"等

終りに

実は筆者自身は実装まで分けてクロスプラットフォーム対応するという条件での開発に遭遇したことはないので、
あまりこのようなパッケージ作成の必要に駆られることはないが、必要な人は結構役に立つのではないかと思う。