📝

C#で、.NET ランタイムのインストールが不要な単一実行ファイルを作成し、WindowsとmacOSで動作させる方法

2024/09/07に公開

はじめに

ツールを作成するときに便利な、単一実行ファイルを作成する方法です。
dotnet環境がインストールされていない環境でも実行できる設定を行なっています。

シェルスクリプトで対応できない規模のプログラムを作成するときに使用します。
また、Unityなどで使用しているソースコードやライブラリを共有して使うこともできます。

dotnet sdk 8.0を使用しています。

dotnet --info
% dotnet --info
.NET SDK:
 Version:           8.0.401
 Commit:            811edcc344
 Workload version:  8.0.400-manifests.b6724b7a
 MSBuild version:   17.11.4+37eb419ad

ランタイム環境:
 OS Name:     Mac OS X
 OS Version:  14.6
 OS Platform: Darwin
 RID:         osx-arm64
 Base Path:   /usr/local/share/dotnet/sdk/8.0.401/

インストール済みの .NET ワークロード:
新しいマニフェストをインストールするときに loose manifests を使用するように構成されています。
表示するインストール済みワークロードはありません。

Host:
  Version:      8.0.8
  Architecture: arm64
  Commit:       08338fcaa5

.NET SDKs installed:
  8.0.401 [/usr/local/share/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 8.0.8 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 8.0.8 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

Other architectures found:
  None

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

コンソールアプリ作成

  1. フォルダーを作る。(例ではconsole_testフォルダ)
  2. dotnet newコマンドでテンプレートを作成する。[1]
% mkdir console_test
% cd console_test 
% dotnet new console --framework net8.0 --use-program-main
テンプレート "コンソール アプリ" が正常に作成されました。

作成後の操作を処理しています...
/Users/hogehoge/dotnet/console_test/console_test.csproj を復元しています:
  復元対象のプロジェクトを決定しています...
  /Users/hogehoge/dotnet/console_test/console_test.csproj を復元しました (33 ミリ秒)。
正常に復元されました。


 % ls
Program.cs		console_test.csproj	obj

設定

  • csprojに単一バイナリ設定を追加する。(PropertyGroupのコメントがある行)[2]
    • 必要に応じてRuntimeIdentifierのターゲットアーキテクチャを変更してください。[3]
  • また、リリースビルド時のみ同一バイナリが生成される設定を追加する。(<Choose>内の設定)[4]
console_test.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <!--単一ファイルの発行を有効にします。 また、dotnet build 時の単一ファイルの警告を有効にします。-->
    <PublishSingleFile>true</PublishSingleFile>
    <!--アプリが自己完結型またはフレームワーク依存であるかを判断します。-->
    <SelfContained>true</SelfContained>
    <!-- ターゲットとする OS と CPU の種類を指定します。 また、既定で <SelfContained>true</SelfContained> が設定されます。-->
    <RuntimeIdentifier>win-x64;osx-arm64</RuntimeIdentifier>
  </PropertyGroup>

  <Choose>
      <!-- リリースビルド時のみ -->
      <When Condition=" '$(Configuration)'=='Release' ">
          <PropertyGroup>
              <!-- C# で同じソースコードから常に同じバイナリを生成する -->
              <!-- https://ufcpp.net/blog/2019/5/deterministicbuilds/ -->
              <!-- https://gist.github.com/ufcpp/f3cc89e8c266997063eb2c633114668a -->
              <!-- このオプションの指定で、タイムスタンプとかを決定論的なハッシュ値に置き換える -->
              <Deterministic>true</Deterministic>
              <!-- 「相対パス化」って機能は実はなくて、「このプロジェクトのパスを、この指定したパスに置き換える」みたいな処理を書くらしい -->
              <DeterministicSourceRoot>/_/</DeterministicSourceRoot>
              <RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\'))</RepoRoot>
              <!-- これが、「あるパスを別のあるパスに置き換える」をやるための設定 -->
              <PathMap>$(RepoRoot)=$(DeterministicSourceRoot)</PathMap>
              <!-- おまけ。PDB 中にソースコード埋め込み -->
              <!-- <EnableSourceLink>true</EnableSourceLink>-->
          </PropertyGroup>
      </When>
  </Choose>

</Project>

ビルド

dotnet publishコマンドで実行ファイルを作成する。

# Mac用ビルド
% dotnet publish console_test.csproj -c Release -r osx-arm64 -o ./Export/osx-arm64
# Windows用ビルド
% dotnet publish console_test.csproj -c Release -r win-x64 -o ./Export/win-x64 

実行

Export/実行したいアーキテクチャー/コマンドを実行する。

% ./Export/osx-arm64/console_test 
Hello, World!

おまけ

ビルドスクリプト

Mac向けのビルドシェルスクリプト

build.sh
#!/bin/bash
# 不要なビルドは#でコメントアウトする
rm -fr ./Export/osx-arm64
dotnet clean -r osx-arm64 
dotnet publish console_test.csproj -c Release -r osx-arm64 -o ./Export/osx-arm64

rm -fr ./Export/win-x64
dotnet clean -r win-x64
dotnet publish console_test.csproj -c Release -r win-x64 -o ./Export/win-x64 

Windows向けのビルドバッチファイル

build.bat
rem 不要なビルドは rem でコメントアウトする
Remove-Item .\Export\osx-arm64 -Recurse
dotnet clean -r osx-arm64 
dotnet publish console_test.csproj -c Release -r osx-arm64 -o ./Export/osx-arm64

Remove-Item .\Export\win-x64 -Recurse
dotnet clean -r win-x64
dotnet publish console_test.csproj -c Release -r win-x64 -o ./Export/win-x64 

実行ファイルのフォルダーを取得する

実行ファイルが置いてあるフォルダーに、設定ファイルやログファイルをなど置く場合にパスを取得する。

        // Environment.GetCommandLineArgs メソッド (System) | Microsoft Learn
        // https://learn.microsoft.com/ja-jp/dotnet/api/system.environment.getcommandlineargs?view=net-8.0
        var envExePath = System.Environment.GetCommandLineArgs()[0];
        // Path.GetDirectoryName メソッド (System.IO) | Microsoft Learn
        // https://learn.microsoft.com/ja-jp/dotnet/api/system.io.path.getdirectoryname?view=net-8.0
        var path = System.IO.Path.GetDirectoryName(envExePath);

        System.Console.WriteLine($@"path = {path}");

コマンドラインのパラメーター解析ライブラリー

コマンドラインのパラメーター解析は、Environment.CommandLineやEnvironment.GetCommandLineArgs()を使用して解析することもできる。
複雑なオプション処理をする場合はライブラリーを使用した方が楽に実現できる。

ConsoleAppFramework

Cocona

脚注
  1. Visual Studio Code を使用して .NET コンソール アプリケーションを作成する - .NET | Microsoft Learn ↩︎

  2. アプリケーション配置用に単一ファイルを作成する - .NET | Microsoft Learn ↩︎

  3. .NET Runtime Identifier (RID) カタログ - .NET | Microsoft Learn ↩︎

  4. C# で、同じソースコードから常に同じバイナリを生成する | ++C++; // 未確認飛行 C ブログ ↩︎

Discussion