2
4

More than 1 year has passed since last update.

.NET6の「単一ファイル」

Posted at

前提

.NETでは何も付けずににpublishをすると、出力先のディレクトリに依存しているアセンブリをコピーします。
そのため、作成したアプリケーションを配布しようとしたときに、複数ファイルを配布する必要があります。
これがあんまり嬉しくない場合があるということで、複数出力されるファイルをまとめるビルドオプションが存在します。

また、単一ファイルといっても、普通の.NETのアセンブリは直接実行可能な機械語ではありません。
中間言語で記述されたバイナリであり、システムにインストールされた直接実行可能なランタイムにホストしてもらって実行されます。
単一ファイルといった場合、.NETのアセンブリだけを纏めるものと、ランタイムまでまとめるものの二種類があるわけです。

出力ファイルに関係するオプション

--runtime

これを指定すると、Nativeのランタイムが出力にコピーされます

デフォルト
System:
- Nativeのランタイム(システムにインストールされている必要がある)
- 標準ライブラリのアセンブリファイル

出力(配布するやつ)
- ランタイムを起動する実行ファイル
- エントリポイントを含むアセンブリファイル
- 標準ライブラリ以外のリンクしているアセンブリファイル
--runtime=
System
- 依存なし(ランタイムがインストールされている必要がない)

出力(配布するやつ)
- Nativeのランタイム
- ランタイムを起動する実行ファイル(NativeHost)
- エントリポイントを含むアセンブリファイル
- 標準ライブラリ以外のリンクしているアセンブリファイル
- 標準ライブラリのアセンブリファイル

PublishSingleFile

これをtrueにすると、標準ライブラリを除く、.NETのアセンブリが纏められます。
このオプションを有効にするためには、ターゲットのOSを指定(--runtime)する必要があります。
ランタイムはまとまってくれないので、単一ファイルと言いつつ単一にはなりません。

PublishSingleFile=true
System
- 依存なし(ランタイムがインストールされている必要がない)

出力(配布するやつ)
- Nativeのランタイム
- バンドルファイル
    - ランタイムを起動する実行ファイル(NativeHost)
    - エントリポイントを含むアセンブリ
    - 標準ライブラリ以外のリンクしているアセンブリ
    - 標準ライブラリのアセンブリ

--self-contained

これをfalseにすると、標準ライブラリやランタイムをシステムに依存させます。
配布すべき出力ファイルの総量が減る代わりに、システムにランタイムがインストールされていないと動きません

--self-contained=false
System
- ランタイム(Native)
- 標準ライブラリのアセンブリ

出力(配布するやつ)
- ランタイムを起動する実行ファイル(NativeHost)
- エントリポイントを含むアセンブリファイル
- 標準ライブラリ以外のリンクしているアセンブリファイル
PublishSingleFile=true&&--self-contained=false
System
- ランタイム(Native)
- 標準ライブラリのアセンブリ

出力(配布するやつ)
- バンドルファイル
    - ランタイムを起動する実行ファイル(NativeHost)
    - エントリポイントを含むアセンブリ
    - 標準ライブラリ以外のリンクしているアセンブリ

IncludeNativeLibrariesForSelfExtract

これをtrueにすると、NativeのRuntimeをまとめるようになります

IncludeNativeLibrariesForSelfExtract=true
System
- 依存なし(ランタイムがインストールされている必要がない)

出力(配布するやつ)
- バンドルファイル
    - ランタイム(Native)
    - ランタイムを起動する実行ファイル(NativeHost)
    - エントリポイントを含むアセンブリ
    - 標準ライブラリ以外のリンクしているアセンブリ
- エントリポイントを含むアセンブリファイル
- 標準ライブラリ以外のリンクしているアセンブリファイル
- 標準ライブラリのアセンブリ
IncludeNativeLibrariesForSelfExtract=true&&PublishSingleFile=true
System
- 依存なし(ランタイムがインストールされている必要がない)

出力(配布するやつ)
- バンドルファイル
    - ランタイム(Native)
    - ランタイムを起動する実行ファイル(NativeHost)
    - エントリポイントを含むアセンブリ
    - 標準ライブラリ以外のリンクしているアセンブリ
    - エントリポイントを含むアセンブリファイル
    - 標準ライブラリ以外のリンクしているアセンブリファイル
    - 標準ライブラリのアセンブリ

これで完全な単一ファイルが実現できた…かのように思えますが、実はこれ、見かけ上だけです。

IncludeNativeLibrariesForSelfExtractは出力(配布時)こそ一つのファイルにまとまっていますが、実行時にはランタイム部分がバンドルから取り出されて、%TEMP%\.netに普通のランタイムのファイルとして展開されます。(DOTNET_BUNDLE_EXTRACT_BASE_DIRで場所の変更は可能)

ということで、単一「実行」ファイルではなく、実質的にはインストーラのようなものだというわけです。とはいえ実用上困るケースは、ぱっと思いつきませんが…

単一ファイルアプリケーションをホストする

.NETアプリケーションは、HostAPIを使ってネイティブアプリからライブラリとして呼び出すことができます。
上記で書いている、「ランタイムを起動する実行ファイル(NativeHost)」の部分を自前実装できるということです。

// https://github.com/dotnet/runtime/blob/v6.0.0-rc.2.21480.5/src/native/corehost/hostfxr.h#L40
#include "./hostfxr.h"

auto hostLib = LoadLibrary("hostfxr.dll");
hostfxr_main_bundle_startupinfo_fn hostfxr_main_bundle_startupinfo
    = GetProcAddress(hostLib, "hostfxr_main_bundle_startupinfo");
hostfxr_main_bundle_startupinfo(...);

このようにすれば、単一アプリケーションとしてpublishしたバンドルファイルをホストできそうです。この方法を取るには、ホストする側のネイティブアプリ+hostfxr.dll+バンドルファイルの3つは最低必要になるので全然単一じゃないですが。

hostfxrを性的にリンクすれば、hostfxr.dllは減らせるかな?

展開処理フロー

hostfxr_main_bundle_startupinfo
-> fx_muxer_t::execute
-> fx_muxer_t::handle_exec_host_command
-> read_config_and_execute
-> execute_app
-> corehost_main
-> corehost_main_init
-> process_manifest_and_extract
-> runner_t::extract
-> extractor_t::extract

単一ファイルのビルド処理フロー

// WIP

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4