見切り発車

とりあえずかきとめたい

MSBuild のカスタムタスクでファイルを生成してビルドに含める

vcxproj に含まれる情報を取得してlibclang で処理する方法を調べていますが 今回は生成したファイルをビルドに含める方法について説明します。

前回:MSBuild でvcxproj に独自の処理を追加する - 見切り発車

独自の処理を作成する

前回の記事では、独自の処理をMSBuild のビルドプロセスに組み込む方法について 独自ターゲットを追加して既定のターゲットとの依存関係を設定するということを 書きました。

独自ターゲット内で実行される独自処理を作成する場合は、カスタムタスクを作成します。

カスタムタスクはMicrosoft.Build.Framework パッケージに含まれる ITask インターフェースを実装したクラスです。Microsoft.Build.Utilities.Core パッケージに含まれるTask クラスを継承してExecute メソッドをオーバーライドすると 手軽に実装が可能です。

これらのパッケージは、NuGet でインストールできます。

カスタムタスクを作成する方法は、以下のリンクにチュートリアルが用意されています。

https://learn.microsoft.com/ja-jp/visualstudio/msbuild/tutorial-custom-task-code-generation

その中で触れられていますが、フレームワークはnetstandard2.0 にする 必要があるそうです。そのため、Microsoft.Build のNuGet パッケージも 最新の17.x.x ではなく15.x.x を選択しました。

ごく単純なカスタムタスククラスは次のようになります。

import Microsoft.Build.Utilities;

namespace MyCustomTask
{
    public class MyCustomTask : Task
    {
        public override bool Execute()
        {
            return true;
        }
    }
}

このカスタムタスクは何もしませんが、ビルドプロセスからExecute メソッドが 呼び出されます。true を返すと処理が正常終了したことを表します。 false を返すとビルドが失敗します。

カスタムタスクをvcxproj に組み込む

カスタムタスクのクラスを含むプロジェクトは、クラスライブラリとして作成します。

ビルドするとdll が出力されるので、これをvcxproj からUsingTask 要素で参照します。

先ほどのMyCustomTask を参照する場合にはUsingTask 要素は次のように指定します。

  <UsingTask
    TaskName="MyCustomTask.MyCustomTask"
    AssemblyFile="dll ファイルのパス"
    />

TaskName にはタスククラスの名前を入力しますが、名前空間を含めたほうが確実です。

AssemblyFile にはdll のパスを入力しますが、相対パスでもフルパスでもどちらでも大丈夫です。

UsingTask 要素を追加したら、ターゲットで次のようにして呼び出せます。

  <Target Name="MyTarget" BeforeTargets="ClCompile">
    <MyCustomTask />
  </Target>

カスタムタスクをNuGet パッケージ化すると組み込みが簡単になりそうですが それについては別の機会にします。

vcxproj とカスタムタスクのパラメータのやりとり

カスタムタスク側は、プロパティを使用してパラメータを受け取ります。 またカスタムプロセスから情報を返す場合にもプロパティを使用します。

プロパティには属性を指定して、必須項目や出力用として扱います。

public class MyCustomTask : Task
{
    // 必須の入力ファイル名
    [Required]
    public string InputFile { get; set; }

    // オプションのサフィックス
    public string Suffix { get; set; }

    // MSBuild に返す出力ファイル名
    [Output]
    public string OutputFile { get; set; }

    public override bool Execute()
    {
        // 入力ファイルが無ければ失敗
        if (!System.IO.File.Exists(InputFile))
        {
            return false;
        }
        // ファイルパスをディレクトリ、ファイル名、拡張子に分割
        string dir = System.IO.Path.GetDirectoryName(InputFile);
        string fileName = System.IO.Path.GetFileNameWithoutExtension(InputFile);
        string ext = System.IO.Path.GetExtension(InputFile);
        // Suffix の指定が無ければ.gen を付与
        if (string.IsNullOrEmpty(Suffix))
        {
            OutputFile = System.IO.Path.Combine(dir, $"{fileName}.gen{ext}")
        }
        // ファイル名と拡張子の間にSuffix を挿入
        else
        {
            OutputFile = System.IO.Path.Combine(dir, $"{fileName}{Suffix}{ext}")
        }
        // ダミーのコードを出力
        System.IO.File.WriteAllText(OutputFile, "namespace { int _{ 0 }; }");

        return true;
    }
}

vcxproj では入力パラメータをカスタムタスク要素の属性で渡します。 カスタムタスクの出力パラメータの受け取りは後述します。

  <Target Name="MyTarget" BeforeTargets="ClCompile">
    <MyCustomTask
      InputFile="%(ClCompile.Identity)"
      />
  </Target>

InputFile に@(ClCompile) を渡した場合には、C++コンパイル対象の ファイル名がセミコロンで連結されたひとつの文字列が入力となりますが、 %(ClCompile.Identity) を指定すると各入力ファイルごとに分割されて、 それぞれの入力ファイル名ごとにカスタムタスクが呼び出されます。 これはMSBuildメタデータバッチ処理という機能によるものです。

生成したファイルをC++ のビルドに含める

独自のターゲットでClCompile という項目にファイル名を追加すると、 そのファイルもC++コンパイル対象になります。

カスタムタスクで生成したファイルをClCompile に追加したい場合は Output という子要素とカスタムタスクの出力パラメータを利用します。

次の例のようにOutput 子要素のTaskParameter に出力パラメータ名、 ItemName にClCompile を指定すると、カスタムタスクのExecute メソッドで プロパティに設定した内容がClCompile に追加されます。

  <Target Name="MyTarget" BeforeTargets="ClCompile">
    <MyCustomTask InputFile="%(ClCompile.Identity)">
      <Output TaskParameter="OutputFile" ItemName="ClCompile">
    </MyCustomTask>
  </Target>

まとめ

MSBuild のカスタムタスクでC++ のコードを生成してビルドに含める方法について説明しました。

今回の内容も、チュートリアルとサンプルプロジェクト、 公式のドキュメントの情報を参照して調べたものです。 時間をかけて読むのは強いなと思いました。