From c5df8d5875dc32b1323c0a6740d2785ea5747fc9 Mon Sep 17 00:00:00 2001 From: MichaelGrafnetter Date: Sun, 23 Apr 2017 16:56:26 +0200 Subject: [PATCH] Upgraded the Invoke-MSBuild to version 2.5.1 --- .../Invoke-MSBuild/Invoke-MsBuild.psd1 | 56 +-- .../Invoke-MSBuild/Invoke-MsBuild.psm1 | 474 ++++++++++++------ 2 files changed, 345 insertions(+), 185 deletions(-) diff --git a/Scripts/Modules/Invoke-MSBuild/Invoke-MsBuild.psd1 b/Scripts/Modules/Invoke-MSBuild/Invoke-MsBuild.psd1 index 8758732..d87b579 100644 --- a/Scripts/Modules/Invoke-MSBuild/Invoke-MsBuild.psd1 +++ b/Scripts/Modules/Invoke-MSBuild/Invoke-MsBuild.psd1 @@ -12,7 +12,10 @@ RootModule = 'Invoke-MsBuild.psm1' # Version number of this module. -ModuleVersion = '2.2.0' +ModuleVersion = '2.5.1' + +# Supported PSEditions +# CompatiblePSEditions = @() # ID used to uniquely identify this module GUID = '8ca20938-b92a-42a1-bf65-f644e16a8d9e' @@ -38,10 +41,10 @@ PowerShellVersion = '2.0' # Minimum version of the Windows PowerShell host required by this module # PowerShellHostVersion = '' -# Minimum version of Microsoft .NET Framework required by this module +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. # DotNetFrameworkVersion = '' -# Minimum version of the common language runtime (CLR) required by this module +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. # CLRVersion = '' # Processor architecture (None, X86, Amd64) required by this module @@ -65,17 +68,17 @@ PowerShellVersion = '2.0' # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess # NestedModules = @() -# Functions to export from this module -FunctionsToExport = '*' +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = @('Invoke-MsBuild') -# Cmdlets to export from this module -CmdletsToExport = '*' +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() # Variables to export from this module -VariablesToExport = '*' +VariablesToExport = @() -# Aliases to export from this module -AliasesToExport = '*' +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() # DSC resources to export from this module # DscResourcesToExport = @() @@ -104,38 +107,7 @@ PrivateData = @{ # IconUri = '' # ReleaseNotes of this module - ReleaseNotes = '- Added LogVerbosityLevel parameter to adjust the verbosity MsBuild uses to write to the log file. -- Fixed bug that prevented us from finding msbuild.exe on some machines. - ----------- -Invoke-MsBuild v2 has the following breaking changes from v1: - - A hash table with several properties is returned instead of a simple $true/$false/$null value. - - The GetLogPath switch is gone and replaced with the WhatIf switch. - -New features in v2 include: - - A build log file containing only build errors is created alongside the regular build log file. - - The errors build log file can be auto-launched on build failure. - - New switch has been added to show the build output in the calling scripts console window (does not work with some 3rd party consoles due to Start-Process cmdlet bug). - - A hash table containing the following properties is now returned: - -+ BuildSucceeded = $true if the build passed, $false if the build failed, and $null if we are not sure. -+ BuildLogFilePath = The path to the builds log file. -+ BuildErrorsLogFilePath = The path to the builds error log file. -+ ItemToBuildFilePath = The item that MsBuild is ran against. -+ CommandUsedToBuild = The full command that is used to invoke MsBuild. This can be useful for inspecting what parameters are passed to MsBuild.exe. -+ Message = A message describing any problems that were encoutered by Invoke-MsBuild. This is typically an empty string unless something went wrong. -+ MsBuildProcess = The process that was used to execute MsBuild.exe. - -Changes to make when updating from v1 to v2: -- To capture/display the build success result, you must change: - Invoke-MsBuild ... -to: - (Invoke-MsBuild ...).BuildSucceeded - -- To get the path where the log file will be created, you must change: - Invoke-MsBuild ... -GetLogPath -to: - (Invoke-MsBuild ... -WhatIf).BuildLogFilePath' + ReleaseNotes = '- Fix to find the "Program Files" location correctly on 32 bit windows without throwing an error.' } # End of PSData hashtable diff --git a/Scripts/Modules/Invoke-MSBuild/Invoke-MsBuild.psm1 b/Scripts/Modules/Invoke-MSBuild/Invoke-MsBuild.psm1 index d078333..2f82d0c 100644 --- a/Scripts/Modules/Invoke-MSBuild/Invoke-MsBuild.psm1 +++ b/Scripts/Modules/Invoke-MSBuild/Invoke-MsBuild.psm1 @@ -5,185 +5,205 @@ 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 a hash table with properties for determining if the build succeeded or not, as well as other information (see the OUTPUTS section for list of properties). 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 + 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 Use32BitMsBuild If this switch is provided, the 32-bit version of MsBuild.exe will be used instead of the 64-bit version when both are available. - + .PARAMETER BuildLogDirectoryPath The directory path to write the build log files to. Defaults to putting the log files in the users temp directory (e.g. C:\Users\[User Name]\AppData\Local\Temp). Use the keyword "PathDirectory" to put the log files in the same directory as the .sln or project file being built. Two log files are generated: one with the complete build log, and one that contains only errors from the build. - + + .PARAMETER LogVerbosity + If set, this will set the verbosity of the build log. Possible values are: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. + .PARAMETER AutoLaunchBuildLogOnFailure If set, this switch will cause the build log to automatically be launched into the default viewer if the build fails. This log file contains all of the build output. NOTE: This switch cannot be used with the PassThru switch. - + .PARAMETER AutoLaunchBuildErrorsLogOnFailure If set, this switch will cause the build errors log to automatically be launched into the default viewer if the build fails. This log file only contains errors from the build output. 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 ShowBuildOutputInNewWindow If set, this switch will cause a command prompt window to be shown in order to view the progress of the build. By default the build output is not shown in any window. NOTE: This switch cannot be used with the ShowBuildOutputInCurrentWindow switch. - + .PARAMETER ShowBuildOutputInCurrentWindow If set, this switch will cause the build process to be started in the existing console window, instead of creating a new one. By default the build output is not shown in any window. NOTE: This switch will override the ShowBuildOutputInNewWindow switch. NOTE: There is a problem with the -NoNewWindow parameter of the Start-Process cmdlet; this is used for the ShowBuildOutputInCurrentWindow switch. The bug is that in some PowerShell consoles, the build output is not directed back to the console calling this function, so nothing is displayed. - To avoid the build process from appearing to hang, PromptForInputBeforeClosing only has an effect with ShowBuildOutputInCurrentWindow when running + To avoid the build process from appearing to hang, PromptForInputBeforeClosing only has an effect with ShowBuildOutputInCurrentWindow when running in the default "ConsoleHost" PowerShell console window, as we know it works properly with that console (it does not in other consoles like ISE, PowerGUI, etc.). - + .PARAMETER PromptForInputBeforeClosing If set, this switch will prompt the user for input after the build completes, and will not continue until the user presses a key. NOTE: This switch only has an effect when used with the ShowBuildOutputInNewWindow and ShowBuildOutputInCurrentWindow switches (otherwise build output is not displayed). NOTE: This switch cannot be used with the PassThru switch. NOTE: The user will need to provide input before execution will return back to the calling script (so do not use this switch for automated builds). - NOTE: To avoid the build process from appearing to hang, PromptForInputBeforeClosing only has an effect with ShowBuildOutputInCurrentWindow when running + NOTE: To avoid the build process from appearing to hang, PromptForInputBeforeClosing only has an effect with ShowBuildOutputInCurrentWindow when running in the default "ConsoleHost" PowerShell console window, as we know it works properly with that console (it does not in other consoles like ISE, PowerGUI, etc.). + .PARAMETER MsBuildFilePath + By default this script will locate and use the latest version of MsBuild.exe on the machine. + If you have MsBuild.exe in a non-standard location, or want to force the use of an older MsBuild.exe version, you may pass in the file path of the MsBuild.exe to use. + + .PARAMETER VisualStudioDeveloperCommandPromptFilePath + By default this script will locate and use the latest version of the Visual Studio Developer Command Prompt to run MsBuild. + If you installed Visual Studio in a non-standard location, or want to force the use of an older Visual Studio Command Prompt version, you may pass in the file path to + the Visual Studio Command Prompt to use. The filename is typically VsDevCmd.bat. + .PARAMETER PassThru If set, this switch will cause the calling 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 + 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 AutoLaunchBuildLogOnFailure, AutoLaunchBuildErrorsLogOnFailure, KeepBuildLogOnSuccessfulBuilds, or PromptForInputBeforeClosing switches. - - .PARAMETER LogVerbosity - If set, this will set the verbosity of the build log. Possible values are: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. .PARAMETER WhatIf If set, the build will not actually be performed. - Instead it will just return the result object containing the file paths that would be created if the build is performed with the same parameters. - + Instead it will just return the result hash table containing the file paths that would be created if the build is performed with the same parameters. + .OUTPUTS - When the -PassThru switch is provided, the process being used to run MsBuild.exe is returned. When the -PassThru switch is not provided, a hash table with the following properties is returned: - + BuildSucceeded = $true if the build passed, $false if the build failed, and $null if we are not sure. BuildLogFilePath = The path to the build's log file. BuildErrorsLogFilePath = The path to the build's error log file. - ItemToBuildFilePath = The item that MsBuild is ran against. - CommandUsedToBuild = The full command that is used to invoke MsBuild. This can be useful for inspecting what parameters are passed to MsBuild.exe. + ItemToBuildFilePath = The item that MsBuild ran against. + CommandUsedToBuild = The full command that was used to invoke MsBuild. This can be useful for inspecting what parameters are passed to MsBuild.exe. Message = A message describing any problems that were encoutered by Invoke-MsBuild. This is typically an empty string unless something went wrong. MsBuildProcess = The process that was used to execute MsBuild.exe. - + BuildDuration = The amount of time the build took to complete, represented as a TimeSpan. + .EXAMPLE $buildResult = Invoke-MsBuild -Path "C:\Some Folder\MySolution.sln" - + if ($buildResult.BuildSucceeded -eq $true) - { Write-Host "Build completed successfully." } - else if (!$buildResult.BuildSucceeded -eq $false) - { Write-Host "Build failed. Check the build log file $($buildResult.BuildLogFilePath) for errors." } - else if ($buildResult.BuildSucceeded -eq $null) - { Write-Host "Unsure if build passed or failed: $($buildResult.Message)" } - + { + Write-Output ("Build completed successfully in {0:N1} seconds." -f $buildResult.BuildDuration.TotalSeconds) + } + elseif ($buildResult.BuildSucceeded -eq $false) + { + Write-Output ("Build failed after {0:N1} seconds. Check the build log file '$($buildResult.BuildLogFilePath)' for errors." -f $buildResult.BuildDuration.TotalSeconds) + } + elseif ($buildResult.BuildSucceeded -eq $null) + { + Write-Output "Unsure if build passed or failed: $($buildResult.Message)" + } + Perform the default MsBuild actions on the Visual Studio solution to build the projects in it, and returns a hash table containing the results. The PowerShell script will halt execution until MsBuild completes. - + .EXAMPLE $process = Invoke-MsBuild -Path "C:\Some Folder\MySolution.sln" -PassThru - + while (!$process.HasExited) { Write-Host "Solution is still buildling..." Start-Sleep -Seconds 1 } - + 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 running MsBuild.exe back to the caller while the build is performed. You can check the process's HasExited property to check if the build has completed yet or not. - - .EXAMPLE + + .EXAMPLE if ((Invoke-MsBuild -Path $pathToSolution).BuildSucceeded -eq $true) { - Write-Host "Build completed successfully." + Write-Output "Build completed successfully." } - + Perfom the build against the file specified at $pathToSolution and checks it for success in a single line. - + .EXAMPLE Invoke-MsBuild -Path "C:\Some Folder\MyProject.csproj" -MsBuildParameters "/target:Clean;Build" -ShowBuildOutputInNewWindow - + 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. - + A window displaying the output from MsBuild will be shown so the user can view the progress of the build without it polluting their current terminal window. + + .EXAMPLE + Invoke-MsBuild -Path "C:\Some Folder\MyProject.csproj" -ShowBuildOutputInCurrentWindow + + Builds the given C# project and displays the output from MsBuild in the current terminal window. + .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" -ShowBuildOutputInNewWindow -PromptForInputBeforeClosing -AutoLaunchBuildLogOnFailure - + 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 after the build completes. 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 -AutoLaunchBuildErrorsLogOnFailure - + 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 errors 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 keyword 'PathDirectory' is used, so 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" -WhatIf - - Returns the result object containing the same property values that would be created if the build was ran with the same parameters. + + Returns the result hash table containing the same property values that would be created if the build was ran with the same parameters. The BuildSucceeded property will be $null since no build will actually be invoked. - This will display all of the returned object's properties and their values. - + This will display all of the returned hash table's properties and their values. + .EXAMPLE Invoke-MsBuild -Path "C:\Some Folder\MyProject.csproj" > $null - - Builds the given C# project, discarding the result object and not displaying its properties. - + + Builds the given C# project, discarding the result hash table and not displaying its properties. + .LINK Project home: https://github.com/deadlydog/Invoke-MsBuild - + .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: 2.2.0 + Version: 2.5.1 #> [CmdletBinding(DefaultParameterSetName="Wait")] param @@ -204,9 +224,9 @@ function Invoke-MsBuild [Alias("LogDirectory","L")] [string] $BuildLogDirectoryPath = $env:Temp, - [parameter(Mandatory=$false)] + [parameter(Mandatory=$false)] [ValidateSet('q','quiet','m','minimal','n','normal','d','detailed','diag','diagnostic')] - [string] $LogVerbosityLevel = 'normal', + [string] $LogVerbosityLevel = 'normal', [parameter(Mandatory=$false,ParameterSetName="Wait")] [ValidateNotNullOrEmpty()] @@ -230,6 +250,14 @@ function Invoke-MsBuild [parameter(Mandatory=$false,ParameterSetName="Wait")] [switch] $PromptForInputBeforeClosing, + [parameter(Mandatory=$false)] + [ValidateScript({Test-Path $_})] + [string] $MsBuildFilePath, + + [parameter(Mandatory=$false)] + [ValidateScript({Test-Path $_})] + [string] $VisualStudioDeveloperCommandPromptFilePath, + [parameter(Mandatory=$false,ParameterSetName="PassThru")] [switch] $PassThru, @@ -245,33 +273,33 @@ function Invoke-MsBuild # 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 - - # Ignore cultural differences. This is so that when reading version numbers it does not change the '.' to ',' when the OS's language/culture is not English. - [CultureInfo]::CurrentCulture = [CultureInfo]::InvariantCulture - # 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 } + # Ignore cultural differences. This is so that when reading version numbers it does not change the '.' to ',' when the OS's language/culture is not English. + [System.Threading.Thread]::CurrentThread.CurrentCulture = [CultureInfo]::InvariantCulture + + # 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:AutoLaunchBuildLogOnFailure)) { $AutoLaunchBuildErrorsLogOnFailure = $false } - if (!(Test-Path Variable:Private:KeepBuildLogOnSuccessfulBuilds)) { $KeepBuildLogOnSuccessfulBuilds = $false } + if (!(Test-Path Variable:Private:KeepBuildLogOnSuccessfulBuilds)) { $KeepBuildLogOnSuccessfulBuilds = $false } if (!(Test-Path Variable:Private:PromptForInputBeforeClosing)) { $PromptForInputBeforeClosing = $false } - if (!(Test-Path Variable:Private:PassThru)) { $PassThru = $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) } - + # Always get the full path to the Log files directory. $BuildLogDirectoryPath = [System.IO.Path]::GetFullPath($BuildLogDirectoryPath) - + # Local Variables. $solutionFileName = (Get-ItemProperty -Path $Path).Name $buildLogFilePath = (Join-Path -Path $BuildLogDirectoryPath -ChildPath $solutionFileName) + ".msbuild.log" $buildErrorsLogFilePath = (Join-Path -Path $BuildLogDirectoryPath -ChildPath $solutionFileName) + ".msbulid.errors.log" $windowStyleOfNewWindow = if ($ShowBuildOutputInNewWindow) { "Normal" } else { "Hidden" } - # Build our object that will be returned. + # Build our hash table that will be returned. $result = @{} $result.BuildSucceeded = $null $result.BuildLogFilePath = $buildLogFilePath @@ -280,20 +308,21 @@ function Invoke-MsBuild $result.CommandUsedToBuild = [string]::Empty $result.Message = [string]::Empty $result.MsBuildProcess = $null + $result.BuildDuration = [TimeSpan]::Zero # Try and build the solution. try - { + { # Get the verbosity to use for the MsBuild log file. - $verbosityLevel = switch ($LogVerbosityLevel) { - { ($_ -eq "q") -or ($_ -eq "quiet") -or ` - ($_ -eq "m") -or ($_ -eq "minimal") -or ` - ($_ -eq "n") -or ($_ -eq "normal") -or ` - ($_ -eq "d") -or ($_ -eq "detailed") -or ` - ($_ -eq "diag") -or ($_ -eq "diagnostic") } { ";verbosity=$_" ;break } - default { "" } - } - + $verbosityLevel = switch ($LogVerbosityLevel) { + { ($_ -eq "q") -or ($_ -eq "quiet") -or ` + ($_ -eq "m") -or ($_ -eq "minimal") -or ` + ($_ -eq "n") -or ($_ -eq "normal") -or ` + ($_ -eq "d") -or ($_ -eq "detailed") -or ` + ($_ -eq "diag") -or ($_ -eq "diagnostic") } { ";verbosity=$_" ;break } + default { "" } + } + # Build the arguments to pass to MsBuild. $buildArguments = """$Path"" $MsBuildParameters /fileLoggerParameters:LogFile=""$buildLogFilePath""$verbosityLevel /fileLoggerParameters1:LogFile=""$buildErrorsLogFilePath"";errorsonly" @@ -304,13 +333,26 @@ function Invoke-MsBuild } # Get the path to the MsBuild executable. - $msBuildPath = Get-MsBuildPath -Use32BitMsBuild:$Use32BitMsBuild + $msBuildPath = $MsBuildFilePath + [bool] $msBuildPathWasNotProvided = [string]::IsNullOrEmpty($msBuildPath) + if ($msBuildPathWasNotProvided) + { + $msBuildPath = Get-LatestMsBuildPath -Use32BitMsBuild:$Use32BitMsBuild + } + + # Get the path to the Visual Studio Developer Command Prompt file. + $vsCommandPromptPath = $VisualStudioDeveloperCommandPromptFilePath + [bool] $vsCommandPromptPathWasNotProvided = [string]::IsNullOrEmpty($vsCommandPromptPath) + if ($vsCommandPromptPathWasNotProvided) + { + $vsCommandPromptPath = Get-LatestVisualStudioCommandPromptPath + } # If a VS Command Prompt was found, call MsBuild from that since it sets environmental variables that may be needed to build some projects types (e.g. XNA). - $vsCommandPromptPath = Get-VisualStudioCommandPromptPath - if ($vsCommandPromptPath -ne $null) + [bool] $vsCommandPromptPathWasFound = ![string]::IsNullOrEmpty($vsCommandPromptPath) + if ($vsCommandPromptPathWasFound) { - $cmdArgumentsToRunMsBuild = "/k "" ""$vsCommandPromptPath"" & msbuild " + $cmdArgumentsToRunMsBuild = "/k "" ""$vsCommandPromptPath"" & ""$msBuildPath"" " } # Else the VS Command Prompt was not found, so just build using MsBuild directly. else @@ -320,10 +362,10 @@ function Invoke-MsBuild # Append the MsBuild arguments to pass into cmd.exe in order to do the build. $cmdArgumentsToRunMsBuild += "$buildArguments " - + # If necessary, add a pause to wait for input before exiting the cmd.exe window. # No pausing allowed when using PassThru or not showing the build output. - # The -NoNewWindow parameter of Start-Process does not behave correctly in the ISE and other PowerShell hosts (doesn't display any build output), + # The -NoNewWindow parameter of Start-Process does not behave correctly in the ISE and other PowerShell hosts (doesn't display any build output), # so only allow it if in the default PowerShell host, since we know that one works. $pauseForInput = [string]::Empty if ($PromptForInputBeforeClosing -and !$PassThru ` @@ -333,7 +375,7 @@ function Invoke-MsBuild # Record the exact command used to perform the build to make it easier to troubleshoot issues with builds. $result.CommandUsedToBuild = "cmd.exe $cmdArgumentsToRunMsBuild" - + # If we don't actually want to perform a build, return . if ($WhatIf) { @@ -341,9 +383,9 @@ function Invoke-MsBuild $result.Message = "The '-WhatIf' switch was specified, so a build was not invoked." return $result } - + Write-Debug "Starting new cmd.exe process with arguments ""$cmdArgumentsToRunMsBuild""." - + # Perform the build. if ($PassThru) { @@ -358,16 +400,20 @@ function Invoke-MsBuild } else { - if ($ShowBuildOutputInCurrentWindow) + $performBuildScriptBlock = { - $result.MsBuildProcess = Start-Process cmd.exe -ArgumentList $cmdArgumentsToRunMsBuild -NoNewWindow -PassThru - } - else - { - $result.MsBuildProcess = Start-Process cmd.exe -ArgumentList $cmdArgumentsToRunMsBuild -WindowStyle $windowStyleOfNewWindow -PassThru + if ($ShowBuildOutputInCurrentWindow) + { + $result.MsBuildProcess = Start-Process cmd.exe -ArgumentList $cmdArgumentsToRunMsBuild -NoNewWindow -Wait -PassThru + } + else + { + $result.MsBuildProcess = Start-Process cmd.exe -ArgumentList $cmdArgumentsToRunMsBuild -WindowStyle $windowStyleOfNewWindow -Wait -PassThru + } } - Wait-Process -InputObject $result.MsBuildProcess + # Perform the build and record how long it takes. + $result.BuildDuration = (Measure-Command -Expression $performBuildScriptBlock) } } # If the build crashed, return that the build didn't succeed. @@ -376,20 +422,20 @@ function Invoke-MsBuild $errorMessage = $_ $result.Message = "Unexpected error occurred while building ""$Path"": $errorMessage" $result.BuildSucceeded = $false - - Write-Error ($result.Message) + + Write-Error ($result.Message) return $result } - # 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)) - { + # 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)) + { $result.BuildSucceeded = $null $result.Message = "Cannot find the build log file at '$buildLogFilePath', so unable to determine if build succeeded or not." - - Write-Warning ($result.Message) - return $result - } + + Write-Warning ($result.Message) + return $result + } # 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 $result.MsBuildProcess.ExitCode -eq 0) @@ -398,7 +444,7 @@ function Invoke-MsBuild if ($buildSucceeded) { $result.BuildSucceeded = $true - + # If we shouldn't keep the log files around, delete them. if (!$KeepBuildLogOnSuccessfulBuilds) { @@ -411,7 +457,7 @@ function Invoke-MsBuild { $result.BuildSucceeded = $false $result.Message = "FAILED to build ""$Path"". Please check the build log ""$buildLogFilePath"" for details." - + # Write the error message as a warning. Write-Warning ($result.Message) @@ -434,68 +480,172 @@ function Invoke-MsBuild function Open-BuildLogFileWithDefaultProgram([string]$FilePathToOpen, [ref]$Result) { if (Test-Path -Path $FilePathToOpen -PathType Leaf) - { + { Start-Process -verb "Open" $FilePathToOpen } else - { + { $message = "Could not auto-launch the build log because the expected file does not exist at '$FilePathToOpen'." $Result.Message += [System.Environment]::NewLine + $message Write-Warning $message } } -function Get-VisualStudioCommandPromptPath +function Get-LatestVisualStudioCommandPromptPath { <# .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. #> + [string] $vsCommandPromptPath = Get-VisualStudioCommandPromptPathForVisualStudio2017AndNewer - # We have to use the vswhere.exe tool to locate Visual Studio 2017 - $vsWhere = Join-Path $PSScriptRoot '..\..\Tools\vswhere.exe' - $vs2017Instance = & $vsWhere -nologo -format value -property installationPath -latest -requires 'Microsoft.VisualStudio.Component.VC.CLI.Support' - $vs2017CommandPromptPath = $vs2017Instance + '\Common7\Tools\VsDevCmd.bat' + # If VS 2017 or newer VS Command Prompt was not found, check for older versions of VS Command Prompt. + if ([string]::IsNullOrEmpty($vsCommandPromptPath)) + { + $vsCommandPromptPath = Get-VisualStudioCommandPromptPathForVisualStudio2015AndPrior + } + return $vsCommandPromptPath +} + +function Get-VisualStudioCommandPromptPathForVisualStudio2017AndNewer +{ + # Later we can probably make use of the VSSetup.PowerShell module to find the MsBuild.exe: https://github.com/Microsoft/vssetup.powershell + # Or perhaps the VsWhere.exe: https://github.com/Microsoft/vswhere + # But for now, to keep this script PowerShell 2.0 compatible and not rely on external executables, let's look for it ourselve in known locations. + # Example of known locations: + # "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\Tools\VsDevCmd.bat" + + [string] $visualStudioDirectoryPath = Get-CommonVisualStudioDirectoryPath + [bool] $visualStudioDirectoryPathDoesNotExist = [string]::IsNullOrEmpty($visualStudioDirectoryPath) + if ($visualStudioDirectoryPathDoesNotExist) + { + return $null + } + + # First search for the VS Command Prompt in the expected locations (faster). + $expectedVsCommandPromptPathWithWildcards = "$visualStudioDirectoryPath\*\*\Common7\Tools\VsDevCmd.bat" + $vsCommandPromptPathObjects = Get-Item -Path $expectedVsCommandPromptPathWithWildcards + + [bool] $vsCommandPromptWasNotFound = ($vsCommandPromptPathObjects -eq $null) -or ($vsCommandPromptPathObjects.Length -eq 0) + if ($vsCommandPromptWasNotFound) + { + # Recurisvely search the entire Microsoft Visual Studio directory for the VS Command Prompt (slower, but will still work if MS changes folder structure). + Write-Verbose "The Visual Studio Command Prompt was not found at an expected location. Searching more locations, but this will be a little slow." + $vsCommandPromptPathObjects = Get-ChildItem -Path $visualStudioDirectoryPath -Recurse | Where-Object { $_.Name -ieq 'VsDevCmd.bat' } + } + + $vsCommandPromptPathObjectsSortedWithNewestVersionsFirst = $vsCommandPromptPathObjects | Sort-Object -Property FullName -Descending + + $newestVsCommandPromptPath = $vsCommandPromptPathObjectsSortedWithNewestVersionsFirst | Select-Object -ExpandProperty FullName -First 1 + return $newestVsCommandPromptPath +} + +function Get-VisualStudioCommandPromptPathForVisualStudio2015AndPrior +{ # 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 = @($vs2017CommandPromptPath, $vs2015CommandPromptPath, $vs2013CommandPromptPath, $vs2012CommandPromptPath, $vs2010CommandPromptPath) + $potentialVsCommandPromptPaths = @($vs2015CommandPromptPath, $vs2013CommandPromptPath, $vs2012CommandPromptPath, $vs2010CommandPromptPath) # Store the VS Command Prompt to do the build in, if one exists. - $vsCommandPromptPath = $null - foreach ($path in $vsCommandPromptPaths) + $newestVsCommandPromptPath = $null + foreach ($path in $potentialVsCommandPromptPaths) { - try + [bool] $pathExists = (![string]::IsNullOrEmpty($path)) -and (Test-Path -Path $path -PathType Leaf) + if ($pathExists) { - if (Test-Path -Path $path) - { - $vsCommandPromptPath = $path - break - } + $newestVsCommandPromptPath = $path + break } - catch {} } # Return the path to the VS Command Prompt if it was found. - return $vsCommandPromptPath + return $newestVsCommandPromptPath } -function Get-MsBuildPath([switch] $Use32BitMsBuild) +function Get-LatestMsBuildPath([switch] $Use32BitMsBuild) { <# .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. #> + [string] $msBuildPath = $null + $msBuildPath = Get-MsBuildPathForVisualStudio2017AndNewer -Use32BitMsBuild $Use32BitMsBuild + + # If VS 2017 or newer MsBuild.exe was not found, check for older versions of MsBuild. + if ([string]::IsNullOrEmpty($msBuildPath)) + { + $msBuildPath = Get-MsBuildPathForVisualStudio2015AndPrior -Use32BitMsBuild $Use32BitMsBuild + } + + [bool] $msBuildPathWasNotFound = [string]::IsNullOrEmpty($msBuildPath) + if ($msBuildPathWasNotFound) + { + throw 'Could not determine where to find MsBuild.exe.' + } + + [bool] $msBuildExistsAtThePathFound = (Test-Path $msBuildPath -PathType Leaf) + if(!$msBuildExistsAtThePathFound) + { + throw "MsBuild.exe does not exist at the expected path, '$msBuildPath'." + } + + return $msBuildPath +} + +function Get-MsBuildPathForVisualStudio2017AndNewer([switch] $Use32BitMsBuild) +{ + # Later we can probably make use of the VSSetup.PowerShell module to find the MsBuild.exe: https://github.com/Microsoft/vssetup.powershell + # Or perhaps the VsWhere.exe: https://github.com/Microsoft/vswhere + # But for now, to keep this script PowerShell 2.0 compatible and not rely on external executables, let's look for it ourselve in known locations. + # Example of known locations: + # "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" - 32 bit + # "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\amd64\MSBuild.exe" - 64 bit + + [string] $visualStudioDirectoryPath = Get-CommonVisualStudioDirectoryPath + [bool] $visualStudioDirectoryPathDoesNotExist = [string]::IsNullOrEmpty($visualStudioDirectoryPath) + if ($visualStudioDirectoryPathDoesNotExist) + { + return $null + } + + # First search for MsBuild in the expected 32 and 64 bit locations (faster). + $expected32bitPathWithWildcards = "$visualStudioDirectoryPath\*\*\MsBuild\*\Bin\MsBuild.exe" + $expected64bitPathWithWildcards = "$visualStudioDirectoryPath\*\*\MsBuild\*\Bin\amd64\MsBuild.exe" + $msBuildPathObjects = Get-Item -Path $expected32bitPathWithWildcards, $expected64bitPathWithWildcards + + [bool] $msBuildWasNotFound = ($msBuildPathObjects -eq $null) -or ($msBuildPathObjects.Length -eq 0) + if ($msBuildWasNotFound) + { + # Recurisvely search the entire Microsoft Visual Studio directory for MsBuild (slower, but will still work if MS changes folder structure). + Write-Verbose "MsBuild.exe was not found at an expected location. Searching more locations, but this will be a little slow." + $msBuildPathObjects = Get-ChildItem -Path $visualStudioDirectoryPath -Recurse | Where-Object { $_.Name -ieq 'MsBuild.exe' } + } + + $msBuildPathObjectsSortedWithNewestVersionsFirst = $msBuildPathObjects | Sort-Object -Property FullName -Descending + + $newest32BitMsBuildPath = $msBuildPathObjectsSortedWithNewestVersionsFirst | Where-Object { $_.Directory.Name -ine 'amd64' } | Select-Object -ExpandProperty FullName -First 1 + $newest64BitMsBuildPath = $msBuildPathObjectsSortedWithNewestVersionsFirst | Where-Object { $_.Directory.Name -ieq 'amd64' } | Select-Object -ExpandProperty FullName -First 1 + + if ($Use32BitMsBuild) + { + return $newest32BitMsBuildPath + } + return $newest64BitMsBuildPath +} + +function Get-MsBuildPathForVisualStudio2015AndPrior([switch] $Use32BitMsBuild) +{ $registryPathToMsBuildToolsVersions = 'HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\' if ($Use32BitMsBuild) { @@ -514,22 +664,60 @@ function Get-MsBuildPath([switch] $Use32BitMsBuild) $largestMsBuildToolsVersion = ($msBuildToolsVersions.GetEnumerator() | Sort-Object -Descending -Property Name | Select-Object -First 1).Value $registryPathToMsBuildToolsLatestVersion = Join-Path -Path $registryPathToMsBuildToolsVersions -ChildPath ("{0:n1}" -f $largestMsBuildToolsVersion) $msBuildToolsVersionsKeyToUse = Get-Item -Path $registryPathToMsBuildToolsLatestVersion - $msBuildDirectoryPath = $msBuildToolsVersionsKeyToUse | Get-ItemProperty -Name 'MSBuildToolsPath' | Select -ExpandProperty 'MSBuildToolsPath' + $msBuildDirectoryPath = $msBuildToolsVersionsKeyToUse | Get-ItemProperty -Name 'MSBuildToolsPath' | Select-Object -ExpandProperty 'MSBuildToolsPath' if(!$msBuildDirectoryPath) { - throw 'The registry on this system does not appear to contain the path to the MsBuild.exe directory.' + return $null } - # 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 this system at the path specified in the registry, '$msBuildPath'." - } + # Build the expected path to the MsBuild executable. + $msBuildPath = (Join-Path -Path $msBuildDirectoryPath -ChildPath 'MsBuild.exe') return $msBuildPath } +function Get-CommonVisualStudioDirectoryPath +{ + [string] $programFilesDirectory = $null + try + { + $programFilesDirectory = Get-Item 'Env:\ProgramFiles(x86)' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Value + } + catch + { } + + if ([string]::IsNullOrEmpty($programFilesDirectory)) + { + $programFilesDirectory = 'C:\Program Files (x86)' + } + + # If we're on a 32-bit machine, we need to go straight after the "Program Files" directory. + if (!(Test-Path -Path $programFilesDirectory -PathType Container)) + { + try + { + $programFilesDirectory = Get-Item 'Env:\ProgramFiles' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Value + } + catch + { + $programFilesDirectory = $null + } + + if ([string]::IsNullOrEmpty($programFilesDirectory)) + { + $programFilesDirectory = 'C:\Program Files' + } + } + + [string] $visualStudioDirectoryPath = Join-Path -Path $programFilesDirectory -ChildPath 'Microsoft Visual Studio' + + [bool] $visualStudioDirectoryPathExists = (Test-Path -Path $visualStudioDirectoryPath -PathType Container) + if (!$visualStudioDirectoryPathExists) + { + return $null + } + return $visualStudioDirectoryPath +} + Export-ModuleMember -Function Invoke-MsBuild \ No newline at end of file