Visual Studioでプロジェクトをビルドする際に、複雑な前処理・後処理を記述する場合、通例バッチコマンドによるカスタマイズをします。ただ、Windowsのコマンドは貧弱で、Unix/Linux環境のシェルなどとは比べ物になりません。
従来のバッチコマンドの代わりにPowerShellを使うのが近代的なWindowsプログラマーですが、個人的にはPowerShellの文法が好きではありません。言語機能も従来のバッチコマンドと比べると遥かに柔軟かつ高機能ですが、我々の大好きなC#言語と比べるとかなり書きづらいです。できればPowerShellよりもF#スクリプトやC#スクリプトを使いたいのですが、これらはOS機能として統合・標準化されていないのが難点です。
そこで、PowerShellからC#コンパイラを使い、C#のソースコード文字列を渡してコンパイルし、C#で書かれたクラスをPowerShellから利用するという方法をとってみます。
param($rootDir) $assemblies = ( #"System" # 不要。 "Microsoft.CSharp" # dynamic 型を使用するために必要。 ) $source = @" using System; using System.Runtime.CompilerServices; public static class Test { public static void CheckLambdaSpec() { var data = new[] { 1, 2, 3, 4, 5 }; Action a = null; foreach (var x in data) { a += () => Console.WriteLine(x); } a(); } public static void DoCallerInfoTestImpl([CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) { Console.WriteLine(string.Format("Member name = \"{0}\"", memberName)); Console.WriteLine(string.Format("Source file path = \"{0}\"", sourceFilePath)); Console.WriteLine(string.Format("Source line number = {0}", sourceLineNumber)); } public static void DoCallerInfoTest() { DoCallerInfoTestImpl(); } public static void DoTest(string dir) { //string path = System.IO.Path.Combine(System.IO.Path.Combine(dir, @"Properties"), @"AssemblyInfo.cs"); //using (System.IO.StreamReader reader = new System.IO.StreamReader(path)) var path = System.IO.Path.Combine(dir, @"Properties", @"AssemblyInfo.cs"); using (var reader = new System.IO.StreamReader(path)) { string line; //Console.WriteLine(line?.Length.ToString() ?? "null"); dynamic x = "hoge"; Console.WriteLine(x.Length); while ((line = reader.ReadLine()) != null) { if (line.StartsWith("[assembly: AssemblyVersion(")) { Console.WriteLine(line); break; } } } } } "@ try { # ここでは、プロジェクトの出力先がカレント ディレクトリになる模様。 #[System.Environment]::CurrentDirectory #$PSVersionTable $rootDir $rootDir.Length # コンパイル時に "%LocalAppData%/Temp/" にテンポラリ ソースファイル (*.cs) が生成される模様。 Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $source -Language CSharp #Add-Type -TypeDefinition $source -Language CSharp [Test]::DoTest($rootDir) [Test]::DoCallerInfoTest() #[Test]::CheckLambdaSpec() } catch { #Write-Error($_.Exception) Write-Error($_.Exception.Message) exit -1 }
上記のPowerShellスクリプトをプロジェクトディレクトリに"test.ps1"として保存し、ビルド前あるいはビルド後イベントのコマンドラインとして、
powershell -ExecutionPolicy RemoteSigned -File "$(ProjectDir)test.ps1" "$(ProjectDir)\" ";exit $LASTEXITCODE"
を入力しておきます。
ここで、Visual Studio 環境変数を PowerShell スクリプトのコマンドライン引数として渡すとき、"$(ProjectDir)\" などとします。"$(ProjectDir)" ではダメなようです。末尾になぜか余計なダブルクォーテーションが入ります。
C#コンパイラのバージョン
前述のPowerShellからC#コンパイラを利用する手法自体に関してはWeb上のあちこちで言及されているのですが、そのC#コンパイラのバージョンに関してはほとんど言及がないようです。なので少し調べてみました。
- PowerShell 2.0 環境で使える C# コンパイラでは、C# 2.0 までしか使えない。
- PowerShell 5.1 環境で使える C# コンパイラでは、C# 5.0 まで使える模様。C# 6.0 は使えない模様。Roslyn ではない旧 csc コンパイラらしい?
前述の例はC# 5.0対応コンパイラが使えるという前提で記述しています。そもそもプリインストールされているPowerShellのバージョンもWindows OSによって異なるので、もしプロジェクトでPowerShellスクリプトを使う場合は最小バージョンに合わせて記述するか、開発者全員にPowerShellのバージョンアップを促しましょう。