Invoke-MsBuild.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#Requires -Version 2.0
function Invoke-MsBuild
{
<#
 .SYNOPSIS
 Builds the given Visual Studio solution or project file using MSBuild.
  
 .DESCRIPTION
 Executes the MSBuild.exe tool against the specified Visual Studio solution or project file.
 Returns true if the build succeeded, false if the build failed, and null if we could not determine the build result.
 If using the PathThru switch, the process running MSBuild is returned instead.
  
 .PARAMETER Path
 The path of the Visual Studio solution or project to build (e.g. a .sln or .csproj file).
  
 .PARAMETER MsBuildParameters
 Additional parameters to pass to the MsBuild command-line tool. This can be any valid MsBuild command-line parameters except for the path of
 the solution/project to build.
  
 See http://msdn.microsoft.com/en-ca/library/vstudio/ms164311.aspx for valid MsBuild command-line parameters.
  
 .PARAMETER $BuildLogDirectoryPath
 The directory path to write the build log file to.
 Defaults to putting the log file in the users temp directory (e.g. C:\Users\[User Name]\AppData\Local\Temp).
 Use the keyword "PathDirectory" to put the log file in the same directory as the .sln or project file being built.
  
 .PARAMETER AutoLaunchBuildLog
 If set, this switch will cause the build log to automatically be launched into the default viewer if the build fails.
 NOTE: This switch cannot be used with the PassThru switch.
  
 .PARAMETER KeepBuildLogOnSuccessfulBuilds
 If set, this switch will cause the msbuild log file to not be deleted on successful builds; normally it is only kept around on failed builds.
 NOTE: This switch cannot be used with the PassThru switch.
  
 .PARAMETER ShowBuildWindow
 If set, this switch will cause a command prompt window to be shown in order to view the progress of the build.
  
 .PARAMETER ShowBuildWindowAndPromptForInputBeforeClosing
 If set, this switch will cause a command prompt window to be shown in order to view the progress of the build, and it will remain open
 after the build completes until the user presses a key on it.
 NOTE: If not using PassThru, the user will need to provide input before execution will return back to the calling script.
  
 .PARAMETER PassThru
 If set, this switch will cause the script not to wait until the build (launched in another process) completes before continuing execution.
 Instead the build will be started in a new process and that process will immediately be returned, allowing the calling script to continue
 execution while the build is performed, and also to inspect the process to see when it completes.
 NOTE: This switch cannot be used with the AutoLaunchBuildLog or KeepBuildLogOnSuccessfulBuilds switches.
  
 .PARAMETER GetLogPath
 If set, the build will not actually be performed.
 Instead it will just return the full path of the MsBuild Log file that would be created if the build is performed with the same parameters.
  
 .OUTPUTS
 When the -PassThru switch is not provided, a boolean value is returned; $true indicates that MsBuild completed successfully, $false indicates
 that MsBuild failed with errors (or that something else went wrong), and $null indicates that we were unable to determine if the build succeeded or failed.
  
 When the -PassThru switch is provided, the process being used to run the build is returned.
  
 .EXAMPLE
 $buildSucceeded = Invoke-MsBuild -Path "C:\Some Folder\MySolution.sln"
  
 if ($buildSucceeded)
 { Write-Host "Build completed successfully." }
 else
 { Write-Host "Build failed. Check the build log file for errors." }
  
 Perform the default MSBuild actions on the Visual Studio solution to build the projects in it, and returns whether the build succeeded or failed.
 The PowerShell script will halt execution until MsBuild completes.
  
 .EXAMPLE
 $process = Invoke-MsBuild -Path "C:\Some Folder\MySolution.sln" -PassThru
  
 Perform the default MSBuild actions on the Visual Studio solution to build the projects in it.
 The PowerShell script will not halt execution; instead it will return the process performing MSBuild actions back to the caller while the action is performed.
  
 .EXAMPLE
 Invoke-MsBuild -Path "C:\Some Folder\MyProject.csproj" -MsBuildParameters "/target:Clean;Build" -ShowBuildWindow
  
 Cleans then Builds the given C# project.
 A window displaying the output from MsBuild will be shown so the user can view the progress of the build.
  
 .EXAMPLE
 Invoke-MsBuild -Path "C:\MySolution.sln" -Params "/target:Clean;Build /property:Configuration=Release;Platform=x64;BuildInParallel=true /verbosity:Detailed /maxcpucount"
  
 Cleans then Builds the given solution, specifying to build the project in parallel in the Release configuration for the x64 platform.
 Here the shorter "Params" alias is used instead of the full "MsBuildParameters" parameter name.
  
 .EXAMPLE
 Invoke-MsBuild -Path "C:\Some Folder\MyProject.csproj" -ShowBuildWindowAndPromptForInputBeforeClosing -AutoLaunchBuildLog
  
 Builds the given C# project.
 A window displaying the output from MsBuild will be shown so the user can view the progress of the build, and it will not close until the user
 gives the window some input. This function will also not return until the user gives the window some input, halting the powershell script execution.
 If the build fails, the build log will automatically be opened in the default text viewer.
  
 .EXAMPLE
 Invoke-MsBuild -Path "C:\Some Folder\MyProject.csproj" -BuildLogDirectoryPath "C:\BuildLogs" -KeepBuildLogOnSuccessfulBuilds -AutoLaunchBuildLog
  
 Builds the given C# project.
 The build log will be saved in "C:\BuildLogs", and they will not be automatically deleted even if the build succeeds.
 If the build fails, the build log will automatically be opened in the default text viewer.
  
 .EXAMPLE
 Invoke-MsBuild -Path "C:\Some Folder\MyProject.csproj" -BuildLogDirectoryPath PathDirectory
  
 Builds the given C# project.
 The build log will be saved in "C:\Some Folder\", which is the same directory as the project being built (i.e. directory specified in the Path).
  
 .EXAMPLE
 Invoke-MsBuild -Path "C:\Database\Database.dbproj" -P "/t:Deploy /p:TargetDatabase=MyDatabase /p:TargetConnectionString=`"Data Source=DatabaseServerName`;Integrated Security=True`;Pooling=False`" /p:DeployToDatabase=True"
  
 Deploy the Visual Studio Database Project to the database "MyDatabase".
 Here the shorter "P" alias is used instead of the full "MsBuildParameters" parameter name.
 The shorter alias' of the msbuild parameters are also used; "/t" instead of "/target", and "/p" instead of "/property".
  
 .EXAMPLE
 Invoke-MsBuild -Path "C:\Some Folder\MyProject.csproj" -BuildLogDirectoryPath "C:\BuildLogs" -GetLogPath
  
 Returns the full path to the MsBuild Log file that would be created if the build was ran with the same parameters.
 In this example the returned log path might be "C:\BuildLogs\MyProject.msbuild.log".
 If the BuildLogDirectoryPath was not provided, the returned log path might be "C:\Some Folder\MyProject.msbuild.log".
  
 .LINK
 Project home: https://invokemsbuild.codeplex.com
  
 .NOTES
 Name: Invoke-MsBuild
 Author: Daniel Schroeder (originally based on the module at http://geekswithblogs.net/dwdii/archive/2011/05/27/part-2-automating-a-visual-studio-build-with-powershell.aspx)
 Version: 1.6.1
#>

[CmdletBinding(DefaultParameterSetName="Wait")]
param
(
[parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true,HelpMessage="The path to the file to build with MsBuild (e.g. a .sln or .csproj file).")]
[ValidateScript({Test-Path $_})]
[string] $Path,

[parameter(Mandatory=$false)]
[Alias("Params")]
[Alias("P")]
[string] $MsBuildParameters,

[parameter(Mandatory=$false)]
[ValidateNotNullOrEmpty()]
[Alias("L")]
[string] $BuildLogDirectoryPath = $env:Temp,

[parameter(Mandatory=$false,ParameterSetName="Wait")]
[ValidateNotNullOrEmpty()]
[Alias("AutoLaunch")]
[Alias("A")]
[switch] $AutoLaunchBuildLogOnFailure,

[parameter(Mandatory=$false,ParameterSetName="Wait")]
[ValidateNotNullOrEmpty()]
[Alias("Keep")]
[Alias("K")]
[switch] $KeepBuildLogOnSuccessfulBuilds,

[parameter(Mandatory=$false)]
[Alias("Show")]
[Alias("S")]
[switch] $ShowBuildWindow,

[parameter(Mandatory=$false)]
[Alias("Prompt")]
[switch] $ShowBuildWindowAndPromptForInputBeforeClosing,

[parameter(Mandatory=$false,ParameterSetName="PassThru")]
[switch] $PassThru,

[parameter(Mandatory=$false)]
[Alias("Get")]
[Alias("G")]
[switch] $GetLogPath
)

BEGIN { }
END { }
PROCESS
{
# Turn on Strict Mode to help catch syntax-related errors.
# This must come after a script's/function's param section.
# Forces a function to be the first non-comment code to appear in a PowerShell Script/Module.
Set-StrictMode -Version Latest

        # Default the ParameterSet variables that may not have been set depending on which parameter set is being used. This is required for PowerShell v2.0 compatibility.
        if (!(Test-Path Variable:Private:AutoLaunchBuildLogOnFailure)) { $AutoLaunchBuildLogOnFailure = $false }
        if (!(Test-Path Variable:Private:KeepBuildLogOnSuccessfulBuilds)) { $KeepBuildLogOnSuccessfulBuilds = $false }
        if (!(Test-Path Variable:Private:PassThru)) { $PassThru = $false }

# If the keyword was supplied, place the log in the same folder as the solution/project being built.
if ($BuildLogDirectoryPath.Equals("PathDirectory", [System.StringComparison]::InvariantCultureIgnoreCase))
{
$BuildLogDirectoryPath = [System.IO.Path]::GetDirectoryName($Path)
}

# Store the VS Command Prompt to do the build in, if one exists.
$vsCommandPrompt = Get-VisualStudioCommandPromptPath

# Local Variables.
$solutionFileName = (Get-ItemProperty -Path $Path).Name
$buildLogFilePath = (Join-Path $BuildLogDirectoryPath $solutionFileName) + ".msbuild.log"
$windowStyle = if ($ShowBuildWindow -or $ShowBuildWindowAndPromptForInputBeforeClosing) { "Normal" } else { "Hidden" }
$buildCrashed = $false;

# If all we want is the path to the Log file that will be generated, return it.
if ($GetLogPath)
{
return $buildLogFilePath
}

# Try and build the solution.
try
{
# Build the arguments to pass to MsBuild.
$buildArguments = """$Path"" $MsBuildParameters /fileLoggerParameters:LogFile=""$buildLogFilePath"""

# If a VS Command Prompt was found, call MSBuild from that since it sets environmental variables that may be needed to build some projects.
if ($vsCommandPrompt -ne $null)
{
$cmdArgumentsToRunMsBuild = "/k "" ""$vsCommandPrompt"" & msbuild "
}
# Else the VS Command Prompt was not found, so just build using MSBuild directly.
else
{
# Get the path to the MsBuild executable.
$msBuildPath = Get-MsBuildPath
$cmdArgumentsToRunMsBuild = "/k "" ""$msBuildPath"" "
}

# Append the MSBuild arguments to pass into cmd.exe in order to do the build.
$pauseForInput = if ($ShowBuildWindowAndPromptForInputBeforeClosing) { "Pause & " } else { "" }
$cmdArgumentsToRunMsBuild += "$buildArguments & $pauseForInput Exit"" "

Write-Debug "Starting new cmd.exe process with arguments ""$cmdArgumentsToRunMsBuild""."

# Perform the build.
if ($PassThru)
{
return Start-Process cmd.exe -ArgumentList $cmdArgumentsToRunMsBuild -WindowStyle $windowStyle -PassThru
}
else
{
$process = Start-Process cmd.exe -ArgumentList $cmdArgumentsToRunMsBuild -WindowStyle $windowStyle -Wait -PassThru
$processExitCode = $process.ExitCode
}
}
catch
{
$buildCrashed = $true;
$errorMessage = $_
Write-Error ("Unexpected error occurred while building ""$Path"": $errorMessage");
}

# If the build crashed, return that the build didn't succeed.
if ($buildCrashed)
{
return $false
}

        # If we can't find the build's log file in order to inspect it, write a warning and return null.
        if (!(Test-Path -Path $buildLogFilePath))
        {
            Write-Warning "Cannot find the build log file at '$buildLogFilePath', so unable to determine if build succeeded or not."
            return $null
        }

# Get if the build failed or not by looking at the log file.
$buildSucceeded = (((Select-String -Path $buildLogFilePath -Pattern "Build FAILED." -SimpleMatch) -eq $null) -and $processExitCode -eq 0)

# If the build succeeded.
if ($buildSucceeded)
{
# If we shouldn't keep the log around, delete it.
if (!$KeepBuildLogOnSuccessfulBuilds)
{
Remove-Item -Path $buildLogFilePath -Force
}
}
# Else at least one of the projects failed to build.
else
{
# Write the error message as a warning.
Write-Warning "FAILED to build ""$Path"". Please check the build log ""$buildLogFilePath"" for details." 

# If we should show the build log automatically, open it with the default viewer.
if($AutoLaunchBuildLogOnFailure)
{
Start-Process -verb "Open" $buildLogFilePath;
}
}

# Return if the Build Succeeded or Failed.
return $buildSucceeded
}
}

function Get-VisualStudioCommandPromptPath
{
<#
 .SYNOPSIS
  Gets the file path to the latest Visual Studio Command Prompt. Returns $null if a path is not found.
  
 .DESCRIPTION
  Gets the file path to the latest Visual Studio Command Prompt. Returns $null if a path is not found.
#>


# Get some environmental paths.
$vs2015CommandPromptPath = $env:VS140COMNTOOLS + 'VsDevCmd.bat'
$vs2013CommandPromptPath = $env:VS120COMNTOOLS + 'VsDevCmd.bat'
$vs2012CommandPromptPath = $env:VS110COMNTOOLS + 'VsDevCmd.bat'
$vs2010CommandPromptPath = $env:VS100COMNTOOLS + 'vcvarsall.bat'
$vsCommandPromptPaths = @($vs2015CommandPromptPath, $vs2013CommandPromptPath, $vs2012CommandPromptPath, $vs2010CommandPromptPath)

# Store the VS Command Prompt to do the build in, if one exists.
$vsCommandPromptPath = $null
foreach ($path in $vsCommandPromptPaths)
{
try
{
if (Test-Path -Path $path)
{
$vsCommandPromptPath = $path
break
}
}
catch {}
}

# Return the path to the VS Command Prompt if it was found.
return $vsCommandPromptPath
}

function Get-MsBuildPath
{
<#
 .SYNOPSIS
 Gets the path to the latest version of MsBuild.exe. Throws an exception if MSBuild.exe is not found.
  
 .DESCRIPTION
 Gets the path to the latest version of MsBuild.exe. Throws an exception if MSBuild.exe is not found.
#>


# Get the path to the directory that the latest version of MSBuild is in.
$MsBuildToolsVersionsStrings = Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\' | Where-Object { $_ -match '[0-9]+\.[0-9]' } | Select-Object -ExpandProperty PsChildName
[double[]]$MsBuildToolsVersions = $MsBuildToolsVersionsStrings | ForEach-Object { [Convert]::ToDouble($_) }
$LargestMsBuildToolsVersion = $MsBuildToolsVersions | Sort-Object -Descending | Select-Object -First 1 
$MsBuildToolsVersionsKeyToUse = Get-Item -Path ('HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\{0:n1}' -f $LargestMsBuildToolsVersion)
$MsBuildDirectoryPath = $MsBuildToolsVersionsKeyToUse | Get-ItemProperty -Name 'MSBuildToolsPath' | Select -ExpandProperty 'MSBuildToolsPath'

if(!$MsBuildDirectoryPath)
{
throw 'MsBuild.exe was not found on the system.'          
}

# Get the path to the MSBuild executable.
$MsBuildPath = (Join-Path -Path $MsBuildDirectoryPath -ChildPath 'msbuild.exe')

if(!(Test-Path $MsBuildPath -PathType Leaf))
{
throw 'MsBuild.exe was not found on the system.'          
}

return $MsBuildPath
}

Export-ModuleMember -Function Invoke-MsBuild