tech.guitarrapc.cóm

Technical updates

.NET Core 3の Single-file executables を生成する

.NET Core 3.0 では、単一バイナリ(Single-file executables)が生成可能になりました。

github.com

今回はどのようにSingle Executable生成するのか、普段は .NET Core 2.1 でビルドしたいときの分け方、dotnet global tool とビルドを分けること、GitHubリリースへのCIからの配置を見てみます。

目次

TL;DR

  • Single-file executables はプラットフォーム依存が必要。
  • 従来のビルドに /p:PublishSingleFile=true を付ければ単一バイナリが生成できる。
  • ランタイム込みでビルドするなら PublishTrimmed を有効にしてビルドするとサイズが小さくできる。
  • DotNet Global Toolsと共存できないので注意。
  • CIも問題ないので使っていこう。

リポジトリ

今回の記事の内容に該当するソースを置いておきます。

記事中細かいものは都度 gist で提示します。

github.com

.NET Coreアプリケーションを利用するときの従来の展開方法

Single-file executablesを考える前に、従来どのようにアプリケーションをビルド、展開していたか振り返ってみます。

.NET Core なアプリケーションを使うときにはランタイムが必要です。これに対応して、.NET Core 2.2まではランタイムを利用する方法が3つありました。

.NET Core アプリケーション展開 - .NET Core | Microsoft Docs

  • フレームワークに依存する展開 : Framework-dependent deployments (FDD)
  • フレームワークに依存する実行可能ファイル : Framework-dependent executables (FDE)
  • 自己完結型の展開 : Self-contained deployments (SCD)

FDD

多くの場合は FDDでdotnet publish を行って、実行するコンテナや環境に .NET Core Runtime だけ入れていると思います。

ビルドするときに、RIDも--self-containeも指定しません。

dotnet publish -c Release

f:id:guitarrapc_tech:20190818192136p:plain
FDDでのビルド(4ファイル)

実行環境がWindows/Linux/macOSといった様々なプラットフォームであっても環境に依存することなく同じライブラリが使えます。 実行にはdotnet ユーティリティを使います。 ランタイムを含まずアプリケーション/依存ライブラリのみ生成されるので、デプロイ時のサイズも小さくなります。 .NET Core Runtime は後方互換性があるので、最新のランタイムで以前のバージョンも使えたりします。

一方で、アプリケーションが必要とする.NET Core ランタイム以降のバージョンが実行環境にインストールされてないといけません。 .NET Coreが後方互換性のない変更を入れた場合に影響を受ける可能性があります。

FDE

FDDでは実行に dotnet ユーティリティが必要でした。FDEを使うことで、直接実行可能ファイルを呼び出してアプリケーションを実行できます。

ビルドするときに、RIDを指定しつつ --self-containedをfalseにします。

dotnet publish -c Release -r <RID> --self-contained false

f:id:guitarrapc_tech:20190818192808p:plain
FDE(5ファイル)

FDEはFDD同様にランタイムを含まずアプリケーション/依存ライブラリのみ生成されるので、デプロイ時のサイズも小さくなります。 dotnet ユーティリティを使わず起動できます。(Windowsなら .exe、macOS/Linuxなら拡張子なしのファイルが生成されます)

一方で、アプリケーションが必要とする.NET Core ランタイム以降のバージョンが実行環境にインストールされてないといけません。 FDDと異なり、プラットフォーム向けにビルドしているので、アプリをそれぞれ発行しないといけません。

SCD

SCDでdotnet publishを行うと、ビルドパッケージを持っていくだけで使いたい時に利用できます。

ビルドするときに、RIDを指定しつつ --self-containedをtrueにします。

dotnet publish -c Release -r <RID> --self-contained true

f:id:guitarrapc_tech:20190818192318p:plain
SCDでのビルド(217ファイル)

FDDでは実行環境のランタイムの有無で動作できるか依存していました。SCDであれば、そのアプリケーションの動作する.NET Coreランタイムバージョンが含まれているので動作を保証できます。 動作するホストと異なる.NET Coreのバージョンでアプリケーションを組んでいても動作させることができます。

一方で、アプリケーションに実行するプラットフォームごとのランタイムを含むことになるため、サイズが大きくなり、プラットフォームごとにビルドを分ける必要があります。 また、.NET Coreのネイティブ依存関係は展開されないのでホストに入っている前提となります。

core/prereqs.md at master · dotnet/core

課題

マルチプラットフォームで動くこととその前提はわかりました。 従来のビルドでは複数のバイナリファイルが生成されますが、それでどのような課題があるのでしょうか。

個人的な経験では、ファイルが複数あることで前処理、後処理が増えたり考えることが増えると感じます。 CLIやWebアプリケーションを作って動かすときを考えます。

  • わかりにくさ: FDDにおいて実行するためのバイナリと依存バイナリの区別が初見では区別つかない。
  • ファイルコピーの面倒: 複数ファイルをコピーする必要がある。
  • 構造維持の面倒: フォルダ構造を持っていれば、フォルダの構造 + ファイルをコピーする必要がある。
  • 前のファイル状態との差分の面倒: 上書きや入れ替え時に実行ファイルや依存ライブラリの差分に気を付ける必要がある。
  • 展開・利用の手間: 複数のファイル、フォルダだと、利用してもらうときに一度のダウンロードで済むようにtar/zipなどで1ファイルに固めて、利用時に展開する手間が生じる。

いずれもコンテナ内でビルドしてCOPY-FROMでランタイムコンテナに移すとしても、それなりに面倒に感じます。

Single-file executables が解決すること

「単一ファイルをコピー(ダウンロード)して実行する」が可能になります。 そのため、GitHub ReleaseやS3/Blob/GCSなどからのダウンロードして実行する。という利用が楽になりました。

Single-file executables の展開方法

Single-file executables には、.NET Core 3.0以上が必要です。

Single-file executables はプラットフォームごとにビルドが必要です。一方で、ランタイムを含めるか含めないかは選択できます。つまり、FDE、SCDが可能で、FDDができません。

FDE

ランタイムをホストに依存させる場合、通常のビルドはdotnet publish -c Release -r <RID> --self-contained false でした。これをSingle-file executablesにするには、/p:PublishSingleFile=true を追加します。

dotnet publish -r RID --self-contained=false /p:PublishSingleFile=true

Windows、macOS、Linux それぞれ次のようになります。

dotnet publish -r win-x64 --self-contained=false /p:PublishSingleFile=true
dotnet publish -r osx-x64 --self-contained=false /p:PublishSingleFile=true
dotnet publish -r linux-x64 --self-contained=false /p:PublishSingleFile=true

生成されたバイナリは直接実行が可能です。

この時のバイナリサイズはごくごく小さくなります。

f:id:guitarrapc_tech:20190818234439p:plain
162KB

SCD

ランタイムを込みでビルドする場合、通常はdotnet publish -c Release -r <RID> でした。これをSingle-file executablesにするときも、-p:PublishSingleFile=true (あるいは /p:PublishSingleFile=true )を追加します。

dotnet publish -r RID /p:PublishSingleFile=true

Windows、macOS、Linux それぞれ次のようになります。

dotnet publish -r win-x64 /p:PublishSingleFile=true
dotnet publish -r osx-x64 /p:PublishSingleFile=true
dotnet publish -r linux-x64 /p:PublishSingleFile=true

生成されたバイナリは直接実行が可能です。

なお、ランタイム込みで生成したバイナリは60MB超えと大きいです。

f:id:guitarrapc_tech:20190818234550p:plain
67463KB

SCDの場合、-p:PublishTrimmed=true を付けることで不要なDLLを抑制してファイルサイズを25MB程度まで小さくできます。(FDEでは使えない自己完結型ビルド専用のオプションです)

dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true -p:PublishTrimmed=true

f:id:guitarrapc_tech:20190818234943p:plain
25925KB

Single-file executables を生成する

単純にSingle-file executablesを試すならBashかPowerShellで次のコマンドを実行します。

gist.github.com

$ ./bin/out/SingleFileExe.exe
Hello World!

次のような csproj と .cs が生成されています。

gist.github.com

このcsprojで重要なのは、TargetFrameworknetcoreapp3.0 を指定することです。 ほかに目立った変化はありません。

dotnet publish -r win10-x64 --self-contained=false /p:PublishSingleFile=true とビルドのたびに引数をずらずら指定するのが面倒な場合、csprojの PropertyGroupにあらかじめ指定するといいでしょう。 たとえば、毎度ランタイム込みの Single-file executablesをするということであれば、あらかじめPublishSingleFilePublishTrimmedを指定しておきます。

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <OutputType>Exe</OutputType>
    <PublishSingleFile>true</PublishSingleFile>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

これで、ビルドコマンドはdotnet publish -c Release -r win-x64 のみでよくなります。

なお、PublishTrimmedはSCDの時にしか使えないので、条件付けしておくのもいいでしょう。

  <PropertyGroup Condition="'$(SelfContained)' == 'true'">
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

また、pdbを Single-file executablesに含める場合は、次のプロパティを追加しておくといいでしょう。

  <PropertyGroup>
    <IncludeSymbolsInSingleFile>true</IncludeSymbolsInSingleFile>
  </PropertyGroup>

全体でみるとこうなります。

gist.github.com

続いて、しばらく使ってみて出てきたユースケースごとに困りごとを解消していきます。

普段は .NET Core2.1 は開発して配布時にのみビルドする

最新の .NET Core 3.0 preview 8 は Go Liveしていますが、普段の開発はまだまだ .NET Core 2.1や 2.2 が多いでしょう。

Announcing .NET Core 3.0 Preview 8 | .NET Blog

Visual Studio 的にデフォルトの2.1が多いように思います。

ということは、普段は 2.1 でまだ開発しておいて、GitHub Release にだけ ランタイム込みのSingle-file executablesを置きたいということがあるでしょう。

この場合は、csprojを次のように定義すると -p:PublishSingleFile=true を指定したときだけ.NET Core 3.0でSingle-file executablesが生成されます。(ランタイム込みなので、 PublishTrimmedは含ませています。)

gist.github.com

dotnet global tools との分離

私はいくつかdotnet global toolsをリリースしています。 ふと、global toolしているプロジェクトで Single-file executables の対応ビルドをしようとしてみます。

gist.github.com

ビルド時に次のエラーが出ます。

$ dotnet publish -r win-x64 -p:PublishSingleFile=true

C:\Program Files\dotnet\sdk\3.0.100-preview7-012821\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.ILLink.targets(142,5): error MSB4018: The "ComputeManagedAssemblies" task failed unexpectedly. [D:\git\guitarrapc\dotnet-lab\singleexecutable\GlobalToolSingleExecutable\GlobalToolSingleExecutable\GlobalToolSingleExecutable.csproj]
C:\Program Files\dotnet\sdk\3.0.100-preview7-012821\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.ILLink.targets(142,5): error MSB4018: System.IO.FileNotFoundException: Could not find file 'D:\git\guitarrapc\dotnet-lab\singleexecutable\GlobalToolSingleExecutable\GlobalToolSingleExecutable\obj\Debug\netcoreapp3.0\win-x64\GlobalToolSingleExecutable.exe'. [D:\git\guitarrapc\dotnet-lab\singleexecutable\GlobalToolSingleExecutable\GlobalToolSingleExecutable\GlobalToolSingleExecutable.csproj]
C:\Program Files\dotnet\sdk\3.0.100-preview7-012821\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.ILLink.targets(142,5): error MSB4018: File name: 'D:\git\guitarrapc\dotnet-lab\singleexecutable\GlobalToolSingleExecutable\GlobalToolSingleExecutable\obj\Debug\netcoreapp3.0\win-x64\GlobalToolSingleExecutable.exe' [D:\git\guitarrapc\dotnet-lab\singleexecutable\GlobalToolSingleExecutable\GlobalToolSingleExecutable\GlobalToolSingleExecutable.csproj]

dotnet global toolsと Single-file executablesは共存できないため、もし同一プロジェクトでやりたい場合は、条件を付けて分岐するのがいいでしょう。

幸い、PublishSingleFileプロパティがあるので、これを使うと間違いがなく独自プロパティの定義が不要です。

gist.github.com

CircleCIでビルドしたSingle-file executablesをGitHubにリリースする

さて、ビルドはしたもののそれをリリースするのにCIを使うことが多いと思います。私も、Circle CI で ghrを使ってGitHubリリースを行っています。

github.com

GitHub リリースに、プラットフォーム別にバイナリを置いておくと利用しやすいのでそのようにビルドを組んでみます。

gist.github.com

ghrは同一ディレクトリにあるバイナリをまとめてリリースに挙げてくれます。 そこで、csproj の<AssemblyName>MySQLToCsharp_$(RuntimeIdentifier)</AssemblyName> で生成されるバイナリごとにRIDを付けて重複しないようにします。 あとは、CircleCIで .NET Core 3.0コンテナでビルドして、Goコンテナからリリースを行えばokです。

バイナリにバージョンを含めない場合は次のようになります。

f:id:guitarrapc_tech:20190819003319p:plain
ghrでGitHub Releaseにバイナリをリリースする

余談

なお、dotnet core で公式にサポートされるまでは、ILMerge や Costura、warp 、monoのmkbundle などがあり、Single-file Publishに関するデザインでも考慮されています。

designs/design.md at master · dotnet/designs

github.com

github.com

github.com

www.mono-project.com

Ref

docs.microsoft.com

docs.microsoft.com

docs.microsoft.com

github.com

www.hanselman.com

www.hanselman.com

devblogs.microsoft.com