PowershellからMSBuildを動かす
まあ別段MSBuildに限った話ではなくて、外部プロセスを起動したいときは同じ考え方ができますが。
やりたいこと
MSBuild.exe MyApp.sln /t:Rebuild /p:Configuration=Release
みたいなのをラップして
Build-VsProject -Path .\MyApp.sln -Options "/t:Rebuild /p:Configuration=Release"
で実行可能にすることを目指します。
どうやるか
パッと浮かぶのは Start-Process
か Call Operator &
を使う方法。
ただ Start-Process にしろ & にしろ、コンソールからちょろっと使うにはいいけど、 スクリプトで使うのは終了コードの取り方が面倒だったりして辛いかも。
なので System.Diagnostics.Process
と,
プロセス実行時の設定を行うための System.Diagnostics.ProcessStartInfo
を自前で作る方法で行くことにします。
.NETのクラスを使うほうが安心感ありますし(謎。
書いていきましょう
System.Diagnostics.Process
はこんな感じで書ける。
$pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = MSBuild.exe $pinfo.Arguments = "Path\to\MyApp.sln /t:Rebuild /p:Configuration=Release" $pinfo.RedirectStandardOutput = $true $pinfo.RedirectStandardError = $true $pinfo.UseShellExecute = $false $proc = New-Object System.Diagnostics.Process $proc.StartInfo = $pinfo $proc.Start() | Out-Null
$proc.RedirectStandardOutput
プロパティと$proc.RedirectStanderdError
プロパティの設定はお好みで。
このプロパティをtrueにしておくと、起動したプロセスの標準出力を取得できるようになります。
この時の注意点として、RedirectStandardOutputプロパティやRedirectStanderdErrorプロパティをTrueにするときは必ずUseShellExecuteプロパティをFalseに設定する必要があります。しないと例外が飛んできます(http://msdn.microsoft.com/ja-jp/library/system.diagnostics.processstartinfo.redirectstandardoutput(v=vs.110).aspx)。
$stdOut = $proc.StandardOutput.ReadToEnd() $errOut = $proc.StandardError.ReadToEnd()
という感じに使うことができます。
終了コード欲しいよね
実行したプロセスの終了コードは$proc.ExitCode
プロパティで取得可能なので
$proc.WaitForExit()
や$proc.HasExit
プロパティなんかと合わせて使います。
$proc.StartInfo = $pinfo $proc.Start() | Out-Null $proc.WaitForExit() if($proc.ExitCode -eq 0){ Write-Host "Build has Succeedes!!" } else{ Write-Host "Build has failed..." }
さて、これで上手くいくと思うじゃろ?
WaitForExit()の罠
そんなことはなく。
$proc.WaitForExit()
にはハマリポイントがあります。
$pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = MSBuild.exe $pinfo.Arguments = "Path\to\MyApp.sln /t:Rebuild /p:Configuration=Release" $pinfo.RedirectStandardOutput = $true $pinfo.RedirectStandardError = $true $pinfo.UseShellExecute = $false $proc = New-Object System.Diagnostics.Process $proc.StartInfo = $pinfo $proc.Start() | Out-Null $proc.WaitForExit() $stdOut = $proc.StandardOutput.ReadToEnd() $errOut = $proc.StandardError.ReadToEnd()
とか素直に書いてしまうとプロセスが終了せずに固まってしまいます。 原因はMSBuildからの出力によってStandardOutputのバッファいっぱいになり、そこで処理が止まってしまうため。
これをいい感じに吸い出してやる必要があります。そのためにOutputDataReceivedイベントと$proc.BeginOutputReadLine()
を使って非同期に出力を読み取ります。
手順は以下の通り。
- BSBuildの出力を保存する追記可能なストアとしてStringBuilderを用意
- OutputDataReceivedイベントで行いたい処理をスクリプトブロックに記述
- OutputDataReceivedのイベントハンドラを登録
- プロセスを実行
$proc.BeginOutputReadLine()
で読み取り開始- プロセス終了後に読み取った出力を適当に書き出す
この流れで書いたのが以下のコードです。
$proc = New-Object System.Diagnostics.Process $proc.StartInfo = $pinfo # 出力の格納先としてStringBuilderを用意 $stdOutBuilder = New-Object -TypeName System.Text.StringBuilder $stdErrBuilder = New-Object -TypeName System.Text.StringBuilder # イベントを利用して出力を書き出す # まずスクリプトブロックにイベントで行いたい処理を記述 # ここではStringBuilderに出力を格納する $action = { if(![string]::IsNullOrEmpty($EventArgs.Data)){ $Event.MessageData.AppendLine($EventArgs.Data) } } # イベントハンドラを登録 $stdOutEvent = Register-ObjectEvent -InputObject $proc -Action $action -EventName "OutputDataReceived" -MessageData $stdOutBuilder $stdErrEvent = Register-ObjectEvent -InputObject $proc -Action $action -EventName "ErrorDataReceived" -MessageData $stdErrBuilder # プロセスを開始 [void]$proc.Start() # 非同期で出力の読み取りを開始 $proc.BeginOutputReadLine() $proc.BeginErrorReadLine() $proc.WaitForExit() # 最後に読み取った出力を書き出す $stdOutBuilder.ToString() | Out-File ".\BuildLog.txt" $stdErrBuilder.ToString() | Out-File ".\ErrorLog.txt"
思った以上に面倒なコードになりました。
コード全体
というわけで最終的に出来上がった?コードはこんな感じでした。ちょっと高度な関数風にしてみました。
function Build-VsProject { [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="Low")] param ( [Parameter( Mandatory=$true, Position=0 )] [alias("SolutionPath")] [string] $Path, [string] $Options ) begin { $msb="C:\Program Files (x86)\MSBuild\12.0\Bin\MSBuild.exe" } process { if(-not(Test-Path -Path $msb)){throw "MSBuildNotFoundException"} Write-Host "Start Build $Path" $pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = $msb $pinfo.Arguments = "$Path /t:Rebuild /p:Configuration=Release" $pinfo.RedirectStandardOutput=$true $pinfo.RedirectStandardError=$true $pinfo.UseShellExecute=$false $pinfo.CreateNoWindow=$true $proc = New-Object System.Diagnostics.Process $proc.StartInfo = $pinfo $stdOutBuilder = New-Object -TypeName System.Text.StringBuilder $stdErrBuilder = New-Object -TypeName System.Text.StringBuilder $action = { if(![string]::IsNullOrEmpty($EventArgs.Data)){ $Event.MessageData.AppendLine($EventArgs.Data) } } $stdOutEvent = Register-ObjectEvent -InputObject $proc -Action $action -EventName "OutputDataReceived" -MessageData $stdOutBuilder $stdErrEvent = Register-ObjectEvent -InputObject $proc -Action $action -EventName "ErrorDataReceived" -MessageData $stdErrBuilder [void]$proc.Start() $proc.BeginOutputReadLine() $proc.BeginErrorReadLine() $proc.WaitForExit() Unregister-Event -SourceIdentifier $stdOutEvent.Name Unregister-Event -SourceIdentifier $stdErrEvent.Name $stdOutBuilder.ToString() | Out-File ".\BuildLog.txt" $stdErrBuilder.ToString() | Out-File ".\ErrorLog.txt" if($proc.ExitCode -eq 0){ Write-Host "Build has Succeedes!!" } else{ Write-Host "Build has failed..." } return $proc.ExitCode } end { } }
おわりに
みたいなことを書こうとしてるところでぎたぱそ先生がPowerShell で System.Diagnostic.Process にて BeginOutputReadLine() を使う - tech.guitarrapc.cómを投稿しておられるのに気づいて会社でぶったまげたことをご報告致します。
さすがMVP。わかりやすい...脱帽するしかないけど、これからも強く儚く生きていこうと思います。よろしくお願いします。