dotnet publish を理解する

.NET がクロスプラットフォームになってからずいぶん経ちますが、理解して`dotnet publish` を実行しているでしょうか？ 私は結構適当だったので、今回ちゃんと理解してみようと思いました。

　こういうことを考えた背景には、Azure Functions で中の人をやっていると、Bad App つまり、zip に固めたアプリが動かないものだったというケースはログが出なくて苦労したり、原因が単純すぎて逆に問題解決に時間がかかったり意外と厄介者です。

 　ですので、安全に、.NET のパッケージングを行うために、`dotnet publish`の勉強をしておくことは悪くないと思いました。

# dotnet publish の概要

[dotnet publish](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-publish)コマンドは、アプリケーションをホストするために、依存関係を含めたパッケージングをしてくれるコマンドです。`Visual Studio` を使っている人は Visual Studio でも publish をすることが可能です。

# クロスプラットフォームに対応する

.NET はクロスプラットフォームの開発が可能ですので、作ったアプリケーションやライブラリが様々な環境で動作することが想定されており、アプリケーションの作成者もそれを意識することが必要です。

## RID (Runtime Identifier) 

[RID (Runtime Identifier)](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog) はアプリケーションのターゲットプラットフォームを表します。例えば、`linux-x64`, `win-x64`, `osx-x64` などが該当します。例えば、Linux の x64 向けのアプリを Publish したい場合は下記のコマンドを実行することで実現可能です。[PortableRuntimeIdentifierGraph.json](https://github.com/dotnet/sdk/blob/main/src/Layout/redist/PortableRuntimeIdentifierGraph.json)により、現在使用可能な値が分かります。

フォーマットは`[os].[version]-[architecture]-[additional qualifiers]` です。`os` はOSの名前下記の例では `linux` が該当します。`version`はここでは指定されていません。`architecture` はプロセッサーのアーキテクチャで、`x86`, `x64`, `arm`, `arm64`などが該当します。`[additional qualifiers]` さらなる違いで、`aot` などがあります。

```bash
dotnet publish -r linux-x64
```

## 指定可能なオプションとデフォルト

dotnet のアプリーションは、大まかにわけて、実行可能(Windowsではexeファイル)なもの、つまり、`Main` メソッドがあるもの、そして、ライブラリに分割されます。
また、すべてのプラットフォームで動作するクロスプラットフォームか、特定のプラットフォームを指定する形式があります。下記の表に一覧を作ってみました。

  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/b4fe5196-b678-c541-a2d8-e842df2a8f58.png)

### クロスプラットフォーム

クロスプラットフォームの指定は、ライブラリのみに適用されます。例えば、とするだけで良いです。ライブラリではデフォルトでクロスプラットフォームなります。

```bash
dotnet publish
```

実際にこのようにしてパブリッシュしたライブラリのディレクトリは次のようになります。依存ライブラリに含まれるプラットフォーム特有のネイティブライブラリがすべて含まれているようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/b501940c-2ea7-5050-3f04-474f88cbfc81.png)

### Platform specific （プラットフォーム特化）

では、プラットフォーム特化の例を見てみましょう。同じライブラリを`linux-x64`で`publish`してみましょう。これを実行するマシンはWindowsでもかまいません。

```bash
dotnet publish -r linux-x64
```

で `publish` してみます。Windowsではなく、Linuxで使われる so ライブラリが展開されており、他のプラットフォームのライブラリが含まれなくなっています。このように、プラットフォーム特化の形式だとバイナリのサイズを小さくすることが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/64e2e932-59c8-d522-eaa3-2ce93b03fc2e.png)

# deps.json と、runtimeconfig.json

Publish されたライブラリもしくは実行可能なアプリは、`[アプリ名].deps.json` と、`[アプリ名].runtimeconfig.json`(実行可能アプリのみ) が含まれます。中身を見てみましょう。こちらはアプリケーションが実行されるのを期待するdotnet ランタイムの情報が載っています。

```json:ConsoleExeSample.runtimeconfig.json
{
  "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 のバージョンや、期待するプラットフォームの情報も入っています。

```json:ConsoleExample.deps.json
{
  "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](https://github.com/dotnet/sdk/blob/main/documentation/specs/runtime-configuration-file.md) に詳細の仕様が記載されています。

# framework-depenent と Self-Contained

今までご紹介してきたのは`framework-dependent` という形態で、`dotnet runtime` がすでにマシンにインストールされている環境で動作するバイナリです。一方、`self-contained` は、dotnet ランタイムを含むバイナリを作ってくれます。バイナリのサイズは大きくなりますが、ランタイムをインストールしなくても動くので、`Self-Contained` でビルドして、一つのバイナリにパッケージすることもできます。
詳細は [Single-file deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?tabs=cli)をご覧ください。そうすると、インストールがめっちゃくちゃ楽になるので、`go lang`のノリでアプリをリリースできて気持ち良いです。

Self-Containedは基本的に実行可能なアプリケーションに使う方式です。クロスプラットフォームではなく、プラットフォームを特定した形で `publish`することが必要です。（たぶんそうでないと、ものすごくバイナリが大きくなるから）

```
dotnet publish -r win-x64 --self-contained
```
`--self-contained` オプションがあるのとないのとでは、ものすごくサイズが違うのがわかってもらえると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/7644f339-e587-aa9a-b2ff-84bf74719292.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/74b5c358-5bf8-0d52-4155-6ac3655dc26d.png)


尚、ネイティブライブラリを持ったようなアプリで、ある特定のプラットフォームでしか使わないことが分かっているなら、プラットフォームを指定したほうがサイズが有利になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/b0c1ade7-51ac-a0dd-c6ab-146598e4bc9d.png)

こちらが、プラットフォームを指定しない場合 `dotnet publish` を実行した場合です。公式のドキュメントには、実行形式の場合デフォルトは使用中のコンピュータになると書いてありましたが、framework dependent でも、全部のライブラリが入るようですね。こちらは、dotnet の runtime が入っているわけではなく、ここで使われているKafkaのライブラリだけなのですが、それでもかなりのサイズの違いがありますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/5ee55725-0b04-6359-fee6-298c93c2b0e3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/23b053e5-b72d-7b89-7d63-e05a346db9d9.png)

# ロールフォワード

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` をマシンから削除します。そして、確認してみます。

```powershell:dotnet runtime version 7 は存在しない
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]
```

```xml:RollForwardSpike.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
```

今回次の１行を追加してべつの場所に publish してみます。

```xml:RollForwardSpike.csproj
	<RollForward>LatestMajor</RollForward>
```

最初のアプリを実行します。

```powershell: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
```

```powershell:RollForward指定あり
C:\Users\tsushi.REDMOND\source\repos\DotnetPublishSpike\test\publish>.\RollForwardSpike.exe
Hello, World!
```

`runtimeconfig.json` を見てみると、コンフィグが追加されているのが分かります。

```json:RollForwardSpike.runtimeconfig.json
{
  "runtimeOptions": {
    "tfm": "net7.0",
    "rollForward": "LatestMajor",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "7.0.0"
    }
  }
}
```

他にこのロールフォワードの指定は、コマンドラインでもできますし、上記の `runtimeconfig.json` に指定することでも可能です。

```powershell:RollForwardをコマンドで指定
RollForwardSpike.exe --roll-forward LatestMajor
Hello, World!
```

ただし、このテクニックは、あくまで dotnet のランタイムのバージョンが無かった場合に限定されるべきフォールバック機構でしょう。メジャーバージョンを自動で上げると、廃止されたクラスやメソッドにヒットする可能性もあるので危険です。一般的にブレーキングチェンジがないマイナーのアップデートがデフォルトなのもうなずけます。
　できるだけこれには頼らなくて済むうようにしたいものです。

1/14/2024時点の情報ですが、最初は Roll Forward を Win マシンで WSL2 にないランタイムバージョンを使用して publish  -> WLS2 で実施して試したのですが、Linux 環境では、バージョン違ってもなぜかこけず、そのまま実行されました。

# リファレンス

* [dotnet publish リファレンス](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-publish)
* [.NET application publishing overview](https://learn.microsoft.com/en-us/dotnet/core/deploying/)
* [Select .NET Version to use](https://learn.microsoft.com/en-us/dotnet/core/versions/selection#control-roll-forward-behavior)
* [Runtime Configuration Files](https://github.com/dotnet/sdk/blob/main/documentation/specs/runtime-configuration-file.md)