Team Foundation Server 2017 で自動ビルドを組んだときに作った MSBuild を実行する PowerShell


はじめに

Team Foundation Server 2017 のビルドを組んだ のですが、少し困ったことがありました。構成は通常、DebugRelease だと思いますが、 Debug_HogeRelease_HogeDebug_FugaRelease_Fuga などの構成が無数にある状態だったのです。一つのプロジェクトを案件によって少しカスタマイズするときなどに、構成を利用していたんですね。これらの構成を既製の [Visual Studio のビルド] タスクで逐一指定するのは面倒です。今後増えるかもしれませんし。

そこで、PowerShell で何とかすることにしました。PowerShell は、.ps1 ファイルをリポジトリに入れておけば、 [PowerShell] タスクで実行することができます。

.vcxproj を読んで構成名を返す関数

function MyGet2015ApplicationConfiguration($fullname) {
    # .vcxproj をロード
    $xml = [xml](Get-Content $fullname)

    # バージョンをチェック
    if ($xml.Project.ToolsVersion -ne "14.0") {
        return
    }

    # プロジェクトの種類をチェック
    if (-not ($xml.Project.PropertyGroup.ConfigurationType -eq "Application")) {
        return
    }

    # 構成を返す
    return $xml.Project.ItemGroup.ProjectConfiguration.Configuration
}
  • .vcxproj を読んで、含まれる構成名を返します。
  • .vcxproj は XML として読むことができます。
  • ToolsVersion をチェックして、プロジェクトを絞り込みます。リポジトリの中には、Visual Studio 2015 のプロジェクトと Visual Studio 2010 のプロジェクトが混在していました。とりあえず、今回の対象は Visual Studio 2015 のプロジェクトだけでしたので、このように決め打ちで書きました。
  • 似たような感じで ConfigurationType をチェックします。Application のプロジェクトと DynamicLibrary のプロジェクトが混在していたのですが、今回の対象は EXE だけです。DLL や COM は EXE に依存されているので、先行するタスクでビルドされるようにしてあります。幸運にも DLL や COM のプロジェクトには特殊な構成はありませんでした。

MSBuild を実行する関数

function MyExecute($filepath, $argumentlist) {
    # -NoNewWindow オプションでコンソールに出力する
    $proc = Start-Process -FilePath $filepath -ArgumentList $argumentlist -NoNewWindow -PassThru

    # 終了を待ちつつ ExitCode を保持するためのおまじない
    $handle = $proc.Handle

    # 終了を待つ
    $proc.WaitForExit()

    # 終了コードを返す
    return $proc.ExitCode
}
  • MSBuild を実行します。といっても、MSBuild のパスもオプションも引数で渡されます。
  • -NoNewWindow を指定しているのは、TFS でログを見たときに、エラーや警告が見えるようにです。
  • ビルドの成功・失敗を返すのには少し苦労しました。この $handle = $proc.Handle という「おまじない」を書かないと、 ExitCode が失われてしまうようです。理屈は分かりません
  • 実は、終了を待つのにも苦労しました。-Wait を指定する方法では、終了を取り逃して待ちぼうけになってしまうことがあるのです。 WaitForExit() にするとうまく行きました。

メイン

# MSBuild.exe のパスをレジストリから取得
$msbuild = Join-Path (Get-ItemProperty "HKLM:\Software\WOW6432NODE\Microsoft\MSBuild\ToolsVersions\14.0").MSBuildToolsPath "MSBuild.exe"

# .vcxproj を列挙
$path = $args[0] # "ProductHoge/*/*.vcxproj"
$fullnames = Resolve-Path $path
#$path = $args[0] # "ProductHoge"
#$fullnames = Get-ChildItem -Path $path -Filter "*.vcxproj" | % FullName

$match = $args[1] # "(release.+|.+release)"

$exitcode = 0
$fullnames | ForEach-Object {
    $fullname = $_
    MyGet2015ApplicationConfiguration $fullname | Where-Object -FilterScript { $_ -Match $match } | ForEach-Object {
        if ((MyExecute $msbuild "$fullname /p:configuration=$_") -ne 0) {
            $exitcode = 1
        }
    }
}
exit $exitcode
  • MSBuild のパスはレジストリから取得します。このレジストリ パスは Visual Studio 2015 の場合です。ビルド エージェントによってインストール先が違うかもしれませんから、さすがに C:\Program Files (x86)\MSBuild\14.0\Bin 決め打ちというわけにはいかないと思いまして。
  • .vcxproj の列挙方法は Resolve-Path にワイルドカードを含むパスを渡す作戦です。.vcxproj が皆、フォルダ階層の同じ深さに置いてあったので、これで行くことにしました。
  • 実は、.vcxproj の列挙方法には迷いがあって、Get-ChildItem で再帰的に探す別解もコメントで残してあります。
  • .vcxproj を列挙し、構成名を列挙したあとに、さらに、構成名を正規表現で絞り込んでいます。標準の構成の Release は既製の [Visual Studio のビルド] タスクでビルドするので、それらを除外するために "(release.+|.+release)" を指定して使っています。
    • 【追記】除外したい特別な構成がある場合は次のように指定しています: "(?!release_nakamura|release_yoichi)(release.+|.+release)"
  • ビルドに失敗しても、最後までトライするようにしています。ビルドが壊れているプロジェクトがいっぺんに洗い出せるように。
  • 全て成功すれば 0 を、一度でも失敗があれば 1 を返します。1(たぶん 0 以外)を返せば、ビルドエラーがあったことを TFS が認識してくれます。