.NET がクロスプラットフォームになってからずいぶん経ちますが、理解してdotnet publish
を実行しているでしょうか? 私は結構適当だったので、今回ちゃんと理解してみようと思いました。
こういうことを考えた背景には、Azure Functions で中の人をやっていると、Bad App つまり、zip に固めたアプリが動かないものだったというケースはログが出なくて苦労したり、原因が単純すぎて逆に問題解決に時間がかかったり意外と厄介者です。
ですので、安全に、.NET のパッケージングを行うために、dotnet publish
の勉強をしておくことは悪くないと思いました。
dotnet publish の概要
dotnet publishコマンドは、アプリケーションをホストするために、依存関係を含めたパッケージングをしてくれるコマンドです。Visual Studio
を使っている人は Visual Studio でも publish をすることが可能です。
クロスプラットフォームに対応する
.NET はクロスプラットフォームの開発が可能ですので、作ったアプリケーションやライブラリが様々な環境で動作することが想定されており、アプリケーションの作成者もそれを意識することが必要です。
RID (Runtime Identifier)
RID (Runtime Identifier) はアプリケーションのターゲットプラットフォームを表します。例えば、linux-x64
, win-x64
, osx-x64
などが該当します。例えば、Linux の x64 向けのアプリを Publish したい場合は下記のコマンドを実行することで実現可能です。PortableRuntimeIdentifierGraph.jsonにより、現在使用可能な値が分かります。
フォーマットは[os].[version]-[architecture]-[additional qualifiers]
です。os
はOSの名前下記の例では linux
が該当します。version
はここでは指定されていません。architecture
はプロセッサーのアーキテクチャで、x86
, x64
, arm
, arm64
などが該当します。[additional qualifiers]
さらなる違いで、aot
などがあります。
dotnet publish -r linux-x64
指定可能なオプションとデフォルト
dotnet のアプリーションは、大まかにわけて、実行可能(Windowsではexeファイル)なもの、つまり、Main
メソッドがあるもの、そして、ライブラリに分割されます。
また、すべてのプラットフォームで動作するクロスプラットフォームか、特定のプラットフォームを指定する形式があります。下記の表に一覧を作ってみました。
クロスプラットフォーム
クロスプラットフォームの指定は、ライブラリのみに適用されます。例えば、とするだけで良いです。ライブラリではデフォルトでクロスプラットフォームなります。
dotnet publish
実際にこのようにしてパブリッシュしたライブラリのディレクトリは次のようになります。依存ライブラリに含まれるプラットフォーム特有のネイティブライブラリがすべて含まれているようになります。
Platform specific (プラットフォーム特化)
では、プラットフォーム特化の例を見てみましょう。同じライブラリをlinux-x64
でpublish
してみましょう。これを実行するマシンはWindowsでもかまいません。
dotnet publish -r linux-x64
で publish
してみます。Windowsではなく、Linuxで使われる so ライブラリが展開されており、他のプラットフォームのライブラリが含まれなくなっています。このように、プラットフォーム特化の形式だとバイナリのサイズを小さくすることが可能です。
deps.json と、runtimeconfig.json
Publish されたライブラリもしくは実行可能なアプリは、[アプリ名].deps.json
と、[アプリ名].runtimeconfig.json
(実行可能アプリのみ) が含まれます。中身を見てみましょう。こちらはアプリケーションが実行されるのを期待するdotnet ランタイムの情報が載っています。
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"configProperties": {
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}
deps.json
には、依存するライブラリのバージョンなどの情報が入っています。依存性の問題などが発生したときはこのファイルを見ると良いでしょう。また、期待するdotnet runtime のバージョンや、期待するプラットフォームの情報も入っています。
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0/win-x64",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {},
".NETCoreApp,Version=v8.0/win-x64": {
"ConsoleExeSample/1.0.0": {
"dependencies": {
"Confluent.Kafka": "1.9.0"
},
"runtime": {
"ConsoleExeSample.dll": {}
}
},
"Confluent.Kafka/1.9.0": {
"dependencies": {
"System.Memory": "4.5.0",
"librdkafka.redist": "1.9.0"
},
"runtime": {
"lib/net5.0/Confluent.Kafka.dll": {
"assemblyVersion": "1.9.0.0",
"fileVersion": "1.9.0.0"
}
}
},
"librdkafka.redist/1.9.0": {
"native": {
"runtimes/win-x64/native/libcrypto-1_1-x64.dll": {
"fileVersion": "1.1.1.14"
},
"runtimes/win-x64/native/libcurl.dll": {
"fileVersion": "7.82.0.0"
},
"runtimes/win-x64/native/librdkafka.dll": {
"fileVersion": "0.0.0.0"
},
"runtimes/win-x64/native/librdkafkacpp.dll": {
"fileVersion": "0.0.0.0"
},
"runtimes/win-x64/native/libssl-1_1-x64.dll": {
"fileVersion": "1.1.1.14"
},
"runtimes/win-x64/native/msvcp140.dll": {
"fileVersion": "14.29.30040.0"
},
"runtimes/win-x64/native/vcruntime140.dll": {
"fileVersion": "14.29.30040.0"
},
"runtimes/win-x64/native/zlib1.dll": {
"fileVersion": "1.2.12.0"
},
"runtimes/win-x64/native/zstd.dll": {
"fileVersion": "1.5.2.0"
}
}
},
"System.Memory/4.5.0": {}
}
},
"libraries": {
"ConsoleExeSample/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Confluent.Kafka/1.9.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-nw69qqZx5lJp6aY3sdxgY19AMYFMvRDewcA7V4Nl2NGZq30gy53KK5n47AbAjRyhew2EFeR2tDYTBT0a/qZMaQ==",
"path": "confluent.kafka/1.9.0",
"hashPath": "confluent.kafka.1.9.0.nupkg.sha512"
},
"librdkafka.redist/1.9.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-LzDiHvNd4ZH4tK0fj0fpptbybS31aYZxOwWtUEeekPN7Tg/S3Dmd4epXGkkX3kbOhVLbGOixoTIfMG5o87N/LQ==",
"path": "librdkafka.redist/1.9.0",
"hashPath": "librdkafka.redist.1.9.0.nupkg.sha512"
},
"System.Memory/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-m0psCSpUxTGfvwyO0i03ajXVhgBqyXlibXz0Mo1dtKGjaHrXFLnuQ8rNBTmWRqbfRjr4eC6Wah4X5FfuFDu5og==",
"path": "system.memory/4.5.0",
"hashPath": "system.memory.4.5.0.nupkg.sha512"
}
}
}
これらの詳細の情報は Runtime Configuration Files に詳細の仕様が記載されています。
framework-depenent と Self-Contained
今までご紹介してきたのはframework-dependent
という形態で、dotnet runtime
がすでにマシンにインストールされている環境で動作するバイナリです。一方、self-contained
は、dotnet ランタイムを含むバイナリを作ってくれます。バイナリのサイズは大きくなりますが、ランタイムをインストールしなくても動くので、Self-Contained
でビルドして、一つのバイナリにパッケージすることもできます。
詳細は Single-file deploymentをご覧ください。そうすると、インストールがめっちゃくちゃ楽になるので、go lang
のノリでアプリをリリースできて気持ち良いです。
Self-Containedは基本的に実行可能なアプリケーションに使う方式です。クロスプラットフォームではなく、プラットフォームを特定した形で publish
することが必要です。(たぶんそうでないと、ものすごくバイナリが大きくなるから)
dotnet publish -r win-x64 --self-contained
--self-contained
オプションがあるのとないのとでは、ものすごくサイズが違うのがわかってもらえると思います。
尚、ネイティブライブラリを持ったようなアプリで、ある特定のプラットフォームでしか使わないことが分かっているなら、プラットフォームを指定したほうがサイズが有利になります。
こちらが、プラットフォームを指定しない場合 dotnet publish
を実行した場合です。公式のドキュメントには、実行形式の場合デフォルトは使用中のコンピュータになると書いてありましたが、framework dependent でも、全部のライブラリが入るようですね。こちらは、dotnet の runtime が入っているわけではなく、ここで使われているKafkaのライブラリだけなのですが、それでもかなりのサイズの違いがありますね。
ロールフォワード
framework dependent の場合、自分のPCに指定されたバージョンのdotnet runtime
が入ってないとどうなるでしょう?もちろん動きません。ない場合は、デフォルトで、dotnet runtime のマイナーバージョンを上げて検索してくれます。例えば、7.0
で作られたアプリがあり、実際に実行マシンに7.1
のdotnet runtime しかない場合、7.1
で実行してくれます。ただ、7.x
はなくて、8.0
だった場合は実行できません。
つまり、マイナーバージョンをデフォルトでアップデートして実行してくれますが、メジャーバージョンはしてくれません。このロールフォワードの挙動は実は変更可能です。
試しに下記のcsproj
ファイルのバイナリをpublish
します。その後、dotnet runtime 7.0
をマシンから削除します。そして、確認してみます。
PS C:\Users\tsushi.REDMOND\source\repos\DotnetPublishSpike> dotnet --list-sdks
3.1.426 [C:\Program Files\dotnet\sdk]
6.0.125 [C:\Program Files\dotnet\sdk]
6.0.320 [C:\Program Files\dotnet\sdk]
8.0.100 [C:\Program Files\dotnet\sdk]
PS C:\Users\tsushi.REDMOND\source\repos\DotnetPublishSpike> dotnet --list-runtimes
Microsoft.AspNetCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 6.0.25 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 8.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 6.0.25 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 8.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 6.0.25 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Microsoft.WindowsDesktop.App 8.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
今回次の1行を追加してべつの場所に publish してみます。
<RollForward>LatestMajor</RollForward>
最初のアプリを実行します。
.\RollForwardSpike.exe
You must install or update .NET to run this application.
App: RollForwardSpike.exe
Architecture: x64
Framework: 'Microsoft.NETCore.App', version '7.0.0' (x64)
.NET location: C:\Program Files\dotnet\
The following frameworks were found:
3.1.32 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
6.0.25 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
8.0.0 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Learn more:
https://aka.ms/dotnet/app-launch-failed
To install missing framework, download:
https://aka.ms/dotnet-core-applaunch?framework=Microsoft.NETCore.App&framework_version=7.0.0&arch=x64&rid=win-x64&os=win10
C:\Users\tsushi.REDMOND\source\repos\DotnetPublishSpike\test\publish>.\RollForwardSpike.exe
Hello, World!
runtimeconfig.json
を見てみると、コンフィグが追加されているのが分かります。
{
"runtimeOptions": {
"tfm": "net7.0",
"rollForward": "LatestMajor",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "7.0.0"
}
}
}
他にこのロールフォワードの指定は、コマンドラインでもできますし、上記の runtimeconfig.json
に指定することでも可能です。
RollForwardSpike.exe --roll-forward LatestMajor
Hello, World!
ただし、このテクニックは、あくまで dotnet のランタイムのバージョンが無かった場合に限定されるべきフォールバック機構でしょう。メジャーバージョンを自動で上げると、廃止されたクラスやメソッドにヒットする可能性もあるので危険です。一般的にブレーキングチェンジがないマイナーのアップデートがデフォルトなのもうなずけます。
できるだけこれには頼らなくて済むうようにしたいものです。
1/14/2024時点の情報ですが、最初は Roll Forward を Win マシンで WSL2 にないランタイムバージョンを使用して publish -> WLS2 で実施して試したのですが、Linux 環境では、バージョン違ってもなぜかこけず、そのまま実行されました。