Archive

Posts Tagged ‘module’

Module to Synchronously Zip and Unzip using PowerShell 2.0

May 2nd, 2015 12 comments

If you search for ways to zip and unzip files using PowerShell, you will find that there a lot of different methods.  Some people invoke .Net 4.5 assembly methods, others call a 3rd party executable (I’ve shown how to do this in one of my other posts).  For my needs this time around I required a method that didn’t involve using 3rd party tools, and wanted my PowerShell script to work on any Windows OS, not just ones that had .Net 4.5 installed (which isn’t available for older OSs like Windows XP).

I quickly found what I was after; you can use the OS native Application.Shell object.  This is what Windows/File Explorer uses behind the scenes when you copy/cut/paste a file.  The problem though was that the operations all happen asynchronously, so there was no way for me to determine when the Zip operation actually completed.  This was a problem, as I wanted my PowerShell script to copy the zip file to a network location once all of the files had been zipped up, and perform other operations on files once they were unzipped from a different zip file, and if I’m zipping/unzipping many MB or GBs or data, the operation might take several minutes.  Most examples I found online worked around this by just putting a Start-Sleep –Seconds 10 after the call to create or extract the Zip files.  That’s a super simple solution, and it works, but I wasn’t always sure how large the directory that I wanted to zip/unzip was going to be, and didn’t want to have my script sleep for 5 minutes when the zip/unzip operation sometimes only takes half a second.  This is what led to me creating the following PowerShell module below.

This module allows you to add files and directories to a new or existing zip file, as well as to extract the contents of a zip file.  Also, it will block script execution until the zip/unzip operation completes.

Here is an example of how to call the 2 public module functions, Compress-ZipFile (i.e. Zip) and Expand-ZipFile (i.e. Unzip):

# If you place the psm1 file in the global PowerShell Modules directory then you could reference it just by name, not by the entire file path like we do here (assumes psm1 file is in same directory as your script).
$THIS_SCRIPTS_DIRECTORY_PATH = Split-Path $script:MyInvocation.MyCommand.Path
$SynchronousZipAndUnzipModulePath = Join-Path $THIS_SCRIPTS_DIRECTORY_PATH 'Synchronous-ZipAndUnzip.psm1'

# Import the Synchronous-ZipAndUnzip module.
Import-Module -Name $SynchronousZipAndUnzipModulePath

# Variables used to test the functions.
$zipFilePath = "C:\Temp\ZipFile.zip"
$filePath = "C:\Test.txt"
$directoryPath = "C:\Test\ZipMeUp"
$destinationDirectoryPath = "C:\Temp\UnzippedContents"

# Create a new Zip file that contains only Test.txt.
Compress-ZipFile -ZipFilePath $zipFilePath -FileOrDirectoryPathToAddToZipFile $filePath -OverwriteWithoutPrompting

# Add the ZipMeUp directory to the zip file.
Compress-ZipFile -ZipFilePath $zipFilePath -FileOrDirectoryPathToAddToZipFile $directoryPath -OverwriteWithoutPrompting

# Unzip the Zip file to a new UnzippedContents directory.
Expand-ZipFile -ZipFilePath $zipFilePath -DestinationDirectoryPath $destinationDirectoryPath -OverwriteWithoutPrompting

 

And here is the Synchronous-ZipAndUnzip.psm1 module code itself:

#Requires -Version 2.0

# Recursive function to calculate the total number of files and directories in the Zip file.
function GetNumberOfItemsInZipFileItems($shellItems)
{
	[int]$totalItems = $shellItems.Count
	foreach ($shellItem in $shellItems)
	{
		if ($shellItem.IsFolder)
		{ $totalItems += GetNumberOfItemsInZipFileItems -shellItems $shellItem.GetFolder.Items() }
	}
	$totalItems
}

# Recursive function to move a directory into a Zip file, since we can move files out of a Zip file, but not directories, and copying a directory into a Zip file when it already exists is not allowed.
function MoveDirectoryIntoZipFile($parentInZipFileShell, $pathOfItemToCopy)
{
	# Get the name of the file/directory to copy, and the item itself.
	$nameOfItemToCopy = Split-Path -Path $pathOfItemToCopy -Leaf
	if ($parentInZipFileShell.IsFolder)
	{ $parentInZipFileShell = $parentInZipFileShell.GetFolder }
	$itemToCopyShell = $parentInZipFileShell.ParseName($nameOfItemToCopy)
	
	# If this item does not exist in the Zip file yet, or it is a file, move it over.
	if ($itemToCopyShell -eq $null -or !$itemToCopyShell.IsFolder)
	{
		$parentInZipFileShell.MoveHere($pathOfItemToCopy)
		
		# Wait for the file to be moved before continuing, to avoid erros about the zip file being locked or a file not being found.
		while (Test-Path -Path $pathOfItemToCopy)
		{ Start-Sleep -Milliseconds 10 }
	}
	# Else this is a directory that already exists in the Zip file, so we need to traverse it and copy each file/directory within it.
	else
	{
		# Copy each file/directory in the directory to the Zip file.
		foreach ($item in (Get-ChildItem -Path $pathOfItemToCopy -Force))
		{
			MoveDirectoryIntoZipFile -parentInZipFileShell $itemToCopyShell -pathOfItemToCopy $item.FullName
		}
	}
}

# Recursive function to move all of the files that start with the File Name Prefix to the Directory To Move Files To.
function MoveFilesOutOfZipFileItems($shellItems, $directoryToMoveFilesToShell, $fileNamePrefix)
{
	# Loop through every item in the file/directory.
	foreach ($shellItem in $shellItems)
	{
		# If this is a directory, recursively call this function to iterate over all files/directories within it.
		if ($shellItem.IsFolder)
		{ 
			$totalItems += MoveFilesOutOfZipFileItems -shellItems $shellItem.GetFolder.Items() -directoryToMoveFilesTo $directoryToMoveFilesToShell -fileNameToMatch $fileNameToMatch
		}
		# Else this is a file.
		else
		{
			# If this file name starts with the File Name Prefix, move it to the specified directory.
			if ($shellItem.Name.StartsWith($fileNamePrefix))
			{
				$directoryToMoveFilesToShell.MoveHere($shellItem)
			}
		}			
	}
}

function Expand-ZipFile
{
	[CmdletBinding()]
	param
	(
		[parameter(Position=1,Mandatory=$true)]
		[ValidateScript({(Test-Path -Path $_ -PathType Leaf) -and $_.EndsWith('.zip', [StringComparison]::OrdinalIgnoreCase)})]
		[string]$ZipFilePath, 
		
		[parameter(Position=2,Mandatory=$false)]
		[string]$DestinationDirectoryPath, 
		
		[Alias("Force")]
		[switch]$OverwriteWithoutPrompting
	)
	
	BEGIN { }
	END { }
	PROCESS
	{	
		# If a Destination Directory was not given, create one in the same directory as the Zip file, with the same name as the Zip file.
		if ($DestinationDirectoryPath -eq $null -or $DestinationDirectoryPath.Trim() -eq [string]::Empty)
		{
			$zipFileDirectoryPath = Split-Path -Path $ZipFilePath -Parent
			$zipFileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($ZipFilePath)
			$DestinationDirectoryPath = Join-Path -Path $zipFileDirectoryPath -ChildPath $zipFileNameWithoutExtension
		}
		
		# If the directory to unzip the files to does not exist yet, create it.
		if (!(Test-Path -Path $DestinationDirectoryPath -PathType Container)) 
		{ New-Item -Path $DestinationDirectoryPath -ItemType Container > $null }

		# Flags and values found at: https://msdn.microsoft.com/en-us/library/windows/desktop/bb759795%28v=vs.85%29.aspx
		$FOF_SILENT = 0x0004
		$FOF_NOCONFIRMATION = 0x0010
		$FOF_NOERRORUI = 0x0400

		# Set the flag values based on the parameters provided.
		$copyFlags = 0
		if ($OverwriteWithoutPrompting)
		{ $copyFlags = $FOF_NOCONFIRMATION }
	#	{ $copyFlags = $FOF_SILENT + $FOF_NOCONFIRMATION + $FOF_NOERRORUI }

		# Get the Shell object, Destination Directory, and Zip file.
	    $shell = New-Object -ComObject Shell.Application
		$destinationDirectoryShell = $shell.NameSpace($DestinationDirectoryPath)
	    $zipShell = $shell.NameSpace($ZipFilePath)
		
		# Start copying the Zip files into the destination directory, using the flags specified by the user. This is an asynchronous operation.
	    $destinationDirectoryShell.CopyHere($zipShell.Items(), $copyFlags)

		# Get the number of files and directories in the Zip file.
		$numberOfItemsInZipFile = GetNumberOfItemsInZipFileItems -shellItems $zipShell.Items()
		
		# The Copy (i.e. unzip) operation is asynchronous, so wait until it is complete before continuing. That is, sleep until the Destination Directory has the same number of files as the Zip file.
		while ((Get-ChildItem -Path $DestinationDirectoryPath -Recurse -Force).Count -lt $numberOfItemsInZipFile)
		{ Start-Sleep -Milliseconds 100 }
	}
}

function Compress-ZipFile
{
	[CmdletBinding()]
	param
	(
		[parameter(Position=1,Mandatory=$true)]
		[ValidateScript({Test-Path -Path $_})]
		[string]$FileOrDirectoryPathToAddToZipFile, 
	
		[parameter(Position=2,Mandatory=$false)]
		[string]$ZipFilePath,
		
		[Alias("Force")]
		[switch]$OverwriteWithoutPrompting
	)
	
	BEGIN { }
	END { }
	PROCESS
	{
		# If a Zip File Path was not given, create one in the same directory as the file/directory being added to the zip file, with the same name as the file/directory.
		if ($ZipFilePath -eq $null -or $ZipFilePath.Trim() -eq [string]::Empty)
		{ $ZipFilePath = Join-Path -Path $FileOrDirectoryPathToAddToZipFile -ChildPath '.zip' }
		
		# If the Zip file to create does not have an extension of .zip (which is required by the shell.application), add it.
		if (!$ZipFilePath.EndsWith('.zip', [StringComparison]::OrdinalIgnoreCase))
		{ $ZipFilePath += '.zip' }
		
		# If the Zip file to add the file to does not exist yet, create it.
		if (!(Test-Path -Path $ZipFilePath -PathType Leaf))
		{ New-Item -Path $ZipFilePath -ItemType File > $null }

		# Get the Name of the file or directory to add to the Zip file.
		$fileOrDirectoryNameToAddToZipFile = Split-Path -Path $FileOrDirectoryPathToAddToZipFile -Leaf

		# Get the number of files and directories to add to the Zip file.
		$numberOfFilesAndDirectoriesToAddToZipFile = (Get-ChildItem -Path $FileOrDirectoryPathToAddToZipFile -Recurse -Force).Count
		
		# Get if we are adding a file or directory to the Zip file.
		$itemToAddToZipIsAFile = Test-Path -Path $FileOrDirectoryPathToAddToZipFile -PathType Leaf

		# Get Shell object and the Zip File.
		$shell = New-Object -ComObject Shell.Application
		$zipShell = $shell.NameSpace($ZipFilePath)

		# We will want to check if we can do a simple copy operation into the Zip file or not. Assume that we can't to start with.
		# We can if the file/directory does not exist in the Zip file already, or it is a file and the user wants to be prompted on conflicts.
		$canPerformSimpleCopyIntoZipFile = $false

		# If the file/directory does not already exist in the Zip file, or it does exist, but it is a file and the user wants to be prompted on conflicts, then we can perform a simple copy into the Zip file.
		$fileOrDirectoryInZipFileShell = $zipShell.ParseName($fileOrDirectoryNameToAddToZipFile)
		$itemToAddToZipIsAFileAndUserWantsToBePromptedOnConflicts = ($itemToAddToZipIsAFile -and !$OverwriteWithoutPrompting)
		if ($fileOrDirectoryInZipFileShell -eq $null -or $itemToAddToZipIsAFileAndUserWantsToBePromptedOnConflicts)
		{
			$canPerformSimpleCopyIntoZipFile = $true
		}
		
		# If we can perform a simple copy operation to get the file/directory into the Zip file.
		if ($canPerformSimpleCopyIntoZipFile)
		{
			# Start copying the file/directory into the Zip file since there won't be any conflicts. This is an asynchronous operation.
			$zipShell.CopyHere($FileOrDirectoryPathToAddToZipFile)	# Copy Flags are ignored when copying files into a zip file, so can't use them like we did with the Expand-ZipFile function.
			
			# The Copy operation is asynchronous, so wait until it is complete before continuing.
			# Wait until we can see that the file/directory has been created.
			while ($zipShell.ParseName($fileOrDirectoryNameToAddToZipFile) -eq $null)
			{ Start-Sleep -Milliseconds 100 }
			
			# If we are copying a directory into the Zip file, we want to wait until all of the files/directories have been copied.
			if (!$itemToAddToZipIsAFile)
			{
				# Get the number of files and directories that should be copied into the Zip file.
				$numberOfItemsToCopyIntoZipFile = (Get-ChildItem -Path $FileOrDirectoryPathToAddToZipFile -Recurse -Force).Count
			
				# Get a handle to the new directory we created in the Zip file.
				$newDirectoryInZipFileShell = $zipShell.ParseName($fileOrDirectoryNameToAddToZipFile)
				
				# Wait until the new directory in the Zip file has the expected number of files and directories in it.
				while ((GetNumberOfItemsInZipFileItems -shellItems $newDirectoryInZipFileShell.GetFolder.Items()) -lt $numberOfItemsToCopyIntoZipFile)
				{ Start-Sleep -Milliseconds 100 }
			}
		}
		# Else we cannot do a simple copy operation. We instead need to move the files out of the Zip file so that we can merge the directory, or overwrite the file without the user being prompted.
		# We cannot move a directory into the Zip file if a directory with the same name already exists, as a MessageBox warning is thrown, not a conflict resolution prompt like with files.
		# We cannot silently overwrite an existing file in the Zip file, as the flags passed to the CopyHere/MoveHere functions seem to be ignored when copying into a Zip file.
		else
		{
			# Create a temp directory to hold our file/directory.
			$tempDirectoryPath = $null
			$tempDirectoryPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetRandomFileName())
			New-Item -Path $tempDirectoryPath -ItemType Container > $null
		
			# If we will be moving a directory into the temp directory.
			$numberOfItemsInZipFilesDirectory = 0
			if ($fileOrDirectoryInZipFileShell.IsFolder)
			{
				# Get the number of files and directories in the Zip file's directory.
				$numberOfItemsInZipFilesDirectory = GetNumberOfItemsInZipFileItems -shellItems $fileOrDirectoryInZipFileShell.GetFolder.Items()
			}
		
			# Start moving the file/directory out of the Zip file and into a temp directory. This is an asynchronous operation.
			$tempDirectoryShell = $shell.NameSpace($tempDirectoryPath)
			$tempDirectoryShell.MoveHere($fileOrDirectoryInZipFileShell)
			
			# If we are moving a directory, we need to wait until all of the files and directories in that Zip file's directory have been moved.
			$fileOrDirectoryPathInTempDirectory = Join-Path -Path $tempDirectoryPath -ChildPath $fileOrDirectoryNameToAddToZipFile
			if ($fileOrDirectoryInZipFileShell.IsFolder)
			{
				# The Move operation is asynchronous, so wait until it is complete before continuing. That is, sleep until the Destination Directory has the same number of files as the directory in the Zip file.
				while ((Get-ChildItem -Path $fileOrDirectoryPathInTempDirectory -Recurse -Force).Count -lt $numberOfItemsInZipFilesDirectory)
				{ Start-Sleep -Milliseconds 100 }
			}
			# Else we are just moving a file, so we just need to check for when that one file has been moved.
			else
			{
				# The Move operation is asynchronous, so wait until it is complete before continuing.
				while (!(Test-Path -Path $fileOrDirectoryPathInTempDirectory))
				{ Start-Sleep -Milliseconds 100 }
			}
			
			# We want to copy the file/directory to add to the Zip file to the same location in the temp directory, so that files/directories are merged.
			# If we should automatically overwrite files, do it.
			if ($OverwriteWithoutPrompting)
			{ Copy-Item -Path $FileOrDirectoryPathToAddToZipFile -Destination $tempDirectoryPath -Recurse -Force }
			# Else the user should be prompted on each conflict.
			else
			{ Copy-Item -Path $FileOrDirectoryPathToAddToZipFile -Destination $tempDirectoryPath -Recurse -Confirm -ErrorAction SilentlyContinue }	# SilentlyContinue errors to avoid an error for every directory copied.

			# For whatever reason the zip.MoveHere() function is not able to move empty directories into the Zip file, so we have to put dummy files into these directories 
			# and then remove the dummy files from the Zip file after.
			# If we are copying a directory into the Zip file.
			$dummyFileNamePrefix = 'Dummy.File'
			[int]$numberOfDummyFilesCreated = 0
			if ($fileOrDirectoryInZipFileShell.IsFolder)
			{
				# Place a dummy file in each of the empty directories so that it gets copied into the Zip file without an error.
				$emptyDirectories = Get-ChildItem -Path $fileOrDirectoryPathInTempDirectory -Recurse -Force -Directory | Where-Object { (Get-ChildItem -Path $_ -Force) -eq $null }
				foreach ($emptyDirectory in $emptyDirectories)
				{
					$numberOfDummyFilesCreated++
					New-Item -Path (Join-Path -Path $emptyDirectory.FullName -ChildPath "$dummyFileNamePrefix$numberOfDummyFilesCreated") -ItemType File -Force > $null
				}
			}		

			# If we need to copy a directory back into the Zip file.
			if ($fileOrDirectoryInZipFileShell.IsFolder)
			{
				MoveDirectoryIntoZipFile -parentInZipFileShell $zipShell -pathOfItemToCopy $fileOrDirectoryPathInTempDirectory
			}
			# Else we need to copy a file back into the Zip file.
			else
			{
				# Start moving the merged file back into the Zip file. This is an asynchronous operation.
				$zipShell.MoveHere($fileOrDirectoryPathInTempDirectory)
			}
			
			# The Move operation is asynchronous, so wait until it is complete before continuing.
			# Sleep until all of the files have been moved into the zip file. The MoveHere() function leaves empty directories behind, so we only need to watch for files.
			do
			{
				Start-Sleep -Milliseconds 100
				$files = Get-ChildItem -Path $fileOrDirectoryPathInTempDirectory -Force -Recurse | Where-Object { !$_.PSIsContainer }
			} while ($files -ne $null)
			
			# If there are dummy files that need to be moved out of the Zip file.
			if ($numberOfDummyFilesCreated -gt 0)
			{
				# Move all of the dummy files out of the supposed-to-be empty directories in the Zip file.
				MoveFilesOutOfZipFileItems -shellItems $zipShell.items() -directoryToMoveFilesToShell $tempDirectoryShell -fileNamePrefix $dummyFileNamePrefix
				
				# The Move operation is asynchronous, so wait until it is complete before continuing.
				# Sleep until all of the dummy files have been moved out of the zip file.
				do
				{
					Start-Sleep -Milliseconds 100
					[Object[]]$files = Get-ChildItem -Path $tempDirectoryPath -Force -Recurse | Where-Object { !$_.PSIsContainer -and $_.Name.StartsWith($dummyFileNamePrefix) }
				} while ($files -eq $null -or $files.Count -lt $numberOfDummyFilesCreated)
			}
			
			# Delete the temp directory that we created.
			Remove-Item -Path $tempDirectoryPath -Force -Recurse > $null
		}
	}
}

# Specify which functions should be publicly accessible.
Export-ModuleMember -Function Expand-ZipFile
Export-ModuleMember -Function Compress-ZipFile

 

Of course if you don’t want to reference an external module you could always just copy paste the functions from the module directly into your script and call the functions that way.

Happy coding!

Disclaimer: At the time of this writing I have only tested the module on Windows 8.1, so if you discover problems running it on another version of Windows please let me know.

Invoke-MsBuild Powershell Module

April 5th, 2013 2 comments

Update: I’ve moved this project to it’s own new home at https://invokemsbuild.codeplex.com.  All updates will be made there.

I’ve spent a little while creating a powershell module that can be used to call MsBuild.  It returns whether the build succeeded or not, and runs through the Visual Studio command prompt if possible, since some projects can’t be built by calling msbuild directly (e.g. XNA projects).  It also provides several other parameters to do things like show the window performing the build, automatically open the build log if the build fails, etc.

Here is the script (copy-paste the code into a file called Invoke-MsBuild.psm1 go download the updated version):

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.
    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.
 
http://msdn.microsoft.com/en-ca/library/vstudio/ms164311.aspx
 
    .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.
     
    .EXAMPLE
    Invoke-MsBuild -Path "C:\Some Folder\MySolution.sln"
     
    Perform the default MSBuild actions on the Visual Studio solution to build the projects in it.
    The PowerShell script will halt execution until MsBuild completes.
     
    .EXAMPLE
    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".
     
    .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.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
 
        # 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
            {
                Start-Process cmd.exe -ArgumentList $cmdArgumentsToRunMsBuild -WindowStyle $windowStyle -Wait
            }
        }
        catch
        {
            $buildCrashed = $true;
            $errorMessage = $_
            Write-Error ("Unexpect error occured while building ""$Path"": $errorMessage" );
        }
 
        # If the build crashed, return that the build didn't succeed.
        if ($buildCrashed)
        {
            return $false
        }
     
        # Get if the build failed or not by looking at the log file.
        $buildSucceeded = ((Select-String -Path $buildLogFilePath -Pattern "Build FAILED." -SimpleMatch) -eq $null)
 
        # 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.
$vs2010CommandPrompt = $env:VS100COMNTOOLS + "vcvarsall.bat"
$vs2012CommandPrompt = $env:VS110COMNTOOLS + "VsDevCmd.bat"
 
# Store the VS Command Prompt to do the build in, if one exists.
$vsCommandPrompt = $null
if (Test-Path $vs2012CommandPrompt)
{
    $vsCommandPrompt = $vs2012CommandPrompt
}
elseif (Test-Path $vs2010CommandPrompt)
{
    $vsCommandPrompt = $vs2010CommandPrompt
}
 
# Return the path to the VS Command Prompt if it was found.
return $vsCommandPrompt
}
 
function Get-MsBuildPath
{
 <#
    .SYNOPSIS
    Gets the path to the latest version of MsBuild.exe. Returns $null if a path is not found.
     
    .DESCRIPTION
    Gets the path to the latest version of MsBuild.exe. Returns $null if a path is not found.
#>
 
# Array of valid MsBuild versions
$Versions = @("4.0", "3.5", "2.0")
 
# Loop through each version from largest to smallest
foreach ($Version in $Versions)
{
    # Try to find an instance of that particular version in the registry
    $RegKey = "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\${Version}"
    $ItemProperty = Get-ItemProperty $RegKey -ErrorAction SilentlyContinue
 
    # If registry entry exsists, then get the msbuild path and retrun
    if ($ItemProperty -ne $null)
    {
        return Join-Path $ItemProperty.MSBuildToolsPath -ChildPath MsBuild.exe
    }
}
 
# Return that we were not able to find MsBuild.exe.
return $null
}
Export-ModuleMember -Function Invoke-MsBuild

 

# Import the module used to build the .sln and project files.
Import-Module -Name [DirectoryContainingModule]\Invoke-MsBuild.psm1
Invoke-MsBuild -Path "[Path to .sln file]" -MsBuildParameters "/target:Clean;Build /property:Configuration=Release;Platform=""Mixed Platforms"" /verbosity:Quiet"

 

And here’s an example of how to use it (assuming you saved it to a file called “Invoke-MsBuild.psm1”:

If you have any suggestions, please comment.

Feel free to use this in your own scripts.  Happy coding!