.NET Core 3.0 では、単一バイナリ(Single-file executables)が生成可能になりました。
今回はどのようにSingle Executable生成するのか、普段は .NET Core 2.1 でビルドしたいときの分け方、dotnet global tool とビルドを分けること、GitHubリリースへのCIからの配置を見てみます。
目次
- 目次
- TL;DR
- リポジトリ
- .NET Coreアプリケーションを利用するときの従来の展開方法
- 課題
- Single-file executables が解決すること
- Single-file executables の展開方法
- Single-file executables を生成する
- 普段は .NET Core2.1 は開発して配布時にのみビルドする
- dotnet global tools との分離
- CircleCIでビルドしたSingle-file executablesをGitHubにリリースする
- 余談
- Ref
TL;DR
- Single-file executables はプラットフォーム依存が必要
- 従来のビルドに
/p:PublishSingleFile=true
を付ければ単一バイナリが生成できる - ランタイム込みでビルドするなら
PublishTrimmed
を有効にしてビルドするとサイズが小さくできる - DotNet Global Toolsと共存できないので注意
- CIも問題ないので使っていこう
リポジトリ
今回の記事の内容に該当するソースを置いておきます。
記事中細かいものは都度 gist で提示します。
.NET Coreアプリケーションを利用するときの従来の展開方法
Single-file executablesを考える前に、従来どのようにアプリケーションをビルド、展開していたか振り返ってみます。
.NET Core なアプリケーションを使うときにはランタイムが必要です。これに対応して、.NET Core 2.2まではランタイムを利用する方法が3つありました。
- フレームワークに依存する展開 : 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
実行環境が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
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
FDDでは実行環境のランタイムの有無で動作できるか依存していました。SCDであれば、そのアプリケーションの動作する.NET Coreランタイムバージョンが含まれているので動作を保証できます。 動作するホストと異なる.NET Coreのバージョンでアプリケーションを組んでいても動作させることができます。
一方で、アプリケーションに実行するプラットフォームごとのランタイムを含むことになるため、サイズが大きくなり、プラットフォームごとにビルドを分ける必要があります。 また、.NET 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
生成されたバイナリは直接実行が可能です。
この時のバイナリサイズはごくごく小さくなります。
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超えと大きいです。
SCDの場合、-p:PublishTrimmed=true
を付けることで不要なDLLを抑制してファイルサイズを25MB程度まで小さくできます。(FDEでは使えない自己完結型ビルド専用のオプションです)
dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true -p:PublishTrimmed=true
Single-file executables を生成する
単純にSingle-file executablesを試すならBashかPowerShellで次のコマンドを実行します。
$ ./bin/out/SingleFileExe.exe Hello World!
次のような csproj と .cs が生成されています。
このcsprojで重要なのは、TargetFramework
に netcoreapp3.0
を指定することです。
ほかに目立った変化はありません。
dotnet publish -r win10-x64 --self-contained=false /p:PublishSingleFile=true
とビルドのたびに引数をずらずら指定するのが面倒な場合、csprojの PropertyGroupにあらかじめ指定するといいでしょう。
たとえば、毎度ランタイム込みの Single-file executablesをするということであれば、あらかじめPublishSingleFile
とPublishTrimmed
を指定しておきます。
<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>
全体でみるとこうなります。
続いて、しばらく使ってみて出てきたユースケースごとに困りごとを解消していきます。
普段は .NET Core2.1 は開発して配布時にのみビルドする
最新の .NET Core 3.0 preview 8 は Go Liveしていますが、普段の開発はまだまだ .NET Core 2.1や 2.2 が多いでしょう。
Visual Studio 的にデフォルトの2.1が多いように思います。
ということは、普段は 2.1 でまだ開発しておいて、GitHub Release にだけ ランタイム込みのSingle-file executablesを置きたいということがあるでしょう。
この場合は、csprojを次のように定義すると -p:PublishSingleFile=true
を指定したときだけ.NET Core 3.0でSingle-file executablesが生成されます。(ランタイム込みなので、 PublishTrimmed
は含ませています。)
dotnet global tools との分離
私はいくつかdotnet global toolsをリリースしています。 ふと、global toolしているプロジェクトで Single-file executables の対応ビルドをしようとしてみます。
ビルド時に次のエラーが出ます。
$ 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
プロパティがあるので、これを使うと間違いがなく独自プロパティの定義が不要です。
CircleCIでビルドしたSingle-file executablesをGitHubにリリースする
さて、ビルドはしたもののそれをリリースするのにCIを使うことが多いと思います。私も、Circle CI で ghrを使ってGitHubリリースを行っています。
GitHub リリースに、プラットフォーム別にバイナリを置いておくと利用しやすいのでそのようにビルドを組んでみます。
ghrは同一ディレクトリにあるバイナリをまとめてリリースに挙げてくれます。
そこで、csproj の<AssemblyName>MySQLToCsharp_$(RuntimeIdentifier)</AssemblyName>
で生成されるバイナリごとにRIDを付けて重複しないようにします。
あとは、CircleCIで .NET Core 3.0コンテナでビルドして、Goコンテナからリリースを行えばokです。
バイナリにバージョンを含めない場合は次のようになります。
余談
なお、dotnet core で公式にサポートされるまでは、ILMerge や Costura、warp 、monoのmkbundle などがあり、Single-file Publishに関するデザインでも考慮されています。
Ref
https://github.com/dotnet/designs/blob/master/accepted/single-file/design.mdgithub.com