環境構築メモ ~ Visual Studio Code + WSL2 + Ubuntu + Docker のコンパイル環境構築時の注意

目的

VS Code での C# コンパイル環境を構築するにあたり、Build は Windows から、Run は Docker コンテナから実行するようにしたい。

C#C++ を混在開発するにあたり、C# の Build までの作業のみ Windows で、それ以外の作業はすべて Docker コンテナで実施したい。初期条件を様々に変更して Run するテスト環境や C++ 開発環境の構築・整備は OS が汚れる可能性があるため、Windows でやりたくない、というのが理由。

概要手順

Docker コンテナから、コマンド1つ叩くと (複数の) C# アプリコンソール開発プロジェクトが整備される環境を構築する。

起動すると複数のフォルダを作成し、1つ1つにテストデータを格納していく Linux ツールがある。それを起点として dotnet コマンドを実行し、C# 開発プロジェクトを作成していく。

Docker コンテナの一般ユーザホームに下記のシェルスクリプトを記述・作成し、コンテナ起動時に実行されるようにする。

function func_dotnet() {
  # メインフォルダの下に各プロジェクトごとのテストデータを配置していくツール (仮に $COMMAND とする)
  ${COMMAND} $1
  cd $1

  for DIR in `find -maxdepth 1 -mindepth 1 -type d`
  do
    cd $DIR
      # .NET Core 3.1 を指定して C# Console App を新規作成
      dotnet new globaljson --sdk-version 3.1.413
      dotnet new console --force

      # Program.cs と .csproj は、デフォルト版を破棄し、事前準備したテンプレート版に差し替え
      rm Program.cs
      rm $DIR.csproj
      cp /cs/Template/* .
      mv ./Program.csproj ./$DIR.csproj

      # NuGet パッケージインストール :【注意点後述】 Windows 環境から Build する場合ここで add package してはいけない (ものもある)
#     dotnet add package ...
    cd ..
  done
}

alias newproj=func_newproj

注意点

フォルダ名に C# C++ などの記号を入れてはならない

記号を入れた場合、OS レベルでは正しく動くが、VS Code が記号を認識できずにデバッガが誤動作する。tasks.json に記述されたデバッガ "problemMatcher": "$msCompile" がフォルダ C# を C までしか認識できないため、エラー行へのクリックジャンプができず、また、うっとおしい警告ポップアップが出るようになる。

Windows / Linux 双方から同一のものを参照するように NuGet パッケージ格納先を変更・共通化する

Windows/Linux 共通で使えるコマンド dotnet add package は、パッケージを格納し、そのパスを .csproj や obj フォルダ内の json に書きこむ。

格納先フォルダが環境により異なるため、そのままでは、build を実行する環境以外でパッケージインストールするとモジュールが見つからなくなる。

  • Windows では %UserProfile%/.nuget/packages
  • Linux では ~/.nuget/packages

そこでまず、%UserProfile%\AppData\Roaming\NuGet\NuGet.Config ファイルに下記の記述を追記し、NuGet パッケージ格納先フォルダを Linux から見える位置に移動する。さらにそのフォルダを Docker コンテナにマウントすることで、Windows / Linux 双方が同一の NuGet パッケージ格納フォルダを参照するようにする。

<configuration>
  ...

  <config>
    <add key="globalPackagesFolder" value="c:\ ...(new folder) ... \.nuget\packages" />
  </config>
</configuration>

それでもまだ注意が必要なものがあり、パッケージ (dll) の中から再帰的に他のパッケージを (動的に?) 呼び出しているものは、build 環境と異なる環境から dotnet add package すると build 時に参照が解決できないと怒られる。

*.csproj の * 部分は格納フォルダ名と同一にする

同一にしない場合、NuGet が obj フォルダ内の json を複数作成したり、取り違えたりするリスクがある。

各プロジェクトへの独自ライブラリ展開は、ソース (.cs) かバイナリ (.dll) か配布方法が異なる

ソース配布の場合はソースフォルダをコピーするだけでよいが、バイナリ配布の場合はソースフォルダをコピーしてはならない。バイナリ作成時の .csproj がプログラム作成用フォルダに展開されるため、コンパイル用の設定が競合する。

バイナリとして配布する場合は、バイナリ作成時に用いた .csproj への参照をプログラム作成用 .csproj を記述してやるだけでよい。

  <ItemGroup>
    <ProjectReference Include="..\..\UserLib\UserLib.csproj" />
  </ItemGroup>
*.csproj に NuGet インストールされるパッケージの参照をあらかじめ書き込まない

事前に手動で書き込んでしまうと、何が正しくインストール・参照設定されたものであるかがわからなくなる。

今回分かったこと

.NET はクロスプラットフォームで便利なものではあるが、実はバイナリ互換性は一部保証されていない。小さな自己完結プログラムであればバイナリ互換であるが、dll 参照などはたとえ dll が揃っていても、環境が異なるとワークしないことがある。

build 環境で Program.exe を走らせると問題ないのに、同じコードから生成したバイナリであっても 、dotnet publish で build したり、実行時動的結合しているであろう dotnet run で走らせると参照エラーになったりする。
.NET Core 2.x までは ILMerge でライブラリを静的結合して exe を生成できたが、.NETCore 3.0 以降は ILMerge は使えず、単一実行ファイル生成するには代わりに .csproj ファイルで Publish 指定する。このときに指定すべき項目を見るとはっきりわかるが、win-x64 や linux などとアーキテクチャを指定しなければならなくなっている。

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <PublishSingleFile>true</PublishSingleFile>
    <SelfContained>true</SelfContained>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PublishReadyToRun>true</PublishReadyToRun>
  </PropertyGroup>

.NET Framework .NET Core → .NET5 → .NET6 と進むにつれ、クロスプラットフォーム化で選択可能なアーキテクチャの幅が広がりつつあるのは望ましいことであるが、それはソースコード互換を意味しており、バイナリ互換で動作することを期待してはいけない*1、ということのようだ。VM なのにちょっと不思議。

*1:異なる環境へのバイナリ・インポートは厳禁であり、リビルドが必要。