目的
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 を実行する環境以外でパッケージインストールするとモジュールが見つからなくなる。
そこでまず、%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:異なる環境へのバイナリ・インポートは厳禁であり、リビルドが必要。