Top 6 Tips for automating your Visual Studio 2013 post-build event with PowerShell

Visual Studio is doing a great job when it comes to building your software. But if you need to do additional steps after building (e.g. steps needed to release the software), the is really only minimal support:

No syntax-highlightning, no intellisense, no resizable splitter... fail!

No syntax-highlightning, no intellisense, no resizable splitter... fail!

You get a minimal Window where you can input a batch-file. And if you open the Macro-pane you even cannot move the splitter... WTF?! So, our first task is to hand over to PowerShell - I use PowerShell ISE here for developing the script.

 

We at code4ward need to be able to release new software versions even faster. This not only goes for the existing Royal TS for Windows product but as well another new product, that we are going to release soon. For this, we need to automate the steps we have to do for building and releasing our software as much as possible. Stefan Koell and I spent a nice afternoon putting together our automation script - and some useful tips came out as well ;)

 

Tip 0: Configure PowerShell to execute scripts (not a tip, really)

Be aware, that Visual Studio 2013 is still a 32-bit process, so you need to use the 32-bit version of PowerShell: On the Start screen type "Windows PowerShell (x86)", execute it as Administrator(!) and issue the following command:

Set-ExecutionPolicy unrestricted

If you run this command as non-admin, you get a nasty exception:


Tip 1: Immediately get out of that post-build event and use PowerShell

We recommend to start PowerShell and do the rest of your logic/code there.
But since Visual Studio is executing this synchronously, you don't see any output in the Output Window until the script has ended, you can start it asynchronously:

powershell "start-process powershell.exe -ArgumentList '-NoExit', '$(ProjectDir)Resources\tools\PostBuild.ps1', '$(ProjectDir)', '$(TargetDir)'"

Lets take this line apart:

  • the first "powershell" starts the script - which immediately starts another script (using start-process).
  • the -ArgumentList is used to pass arguments to the script. -NoExit tells powershell to remain open after the execution finished (so we can check the output). On a build-server you dont want this option. 
  • PostBuild.ps1 is the actual script
  • Since it is located inside the $(ProjectDir) it is also source controlled



Tip 2: Use Visual Studio 2013 Macros in your Powershell

Its handy, that Visual Studio is giving us variables out of the box. we just need to hand them over to the powershell script:

powershell "start-process powershell.exe -ArgumentList '-NoExit', '$(ProjectDir)Resources\tools\PostBuild.ps1', '$(ProjectDir)', '$(TargetDir)'"

The last two parameters are variables from Visual Studio that are passed to the PowerShell script. The PostBuild.ps1 script starts with:

param (
[string]$ProjectDir,
[string]$TargetDir
)

Now you have the macro variables from Visual Studio 2013 in your script available  - no need to hardcode anything ;)


Tip 3: Executing the PostBuild.ps1 only if needed

Now, you probably don't need to execute the script with every build. Using the Visual Studio Configuration Manager you can handle this in a flexible way:


We introduced a "Publish" build target like this:

In the post-build event script editor you can access this information:

if $(ConfigurationName) == Publish (
powershell "start-process powershell.exe -ArgumentList '-NoExit', '$(ProjectDir)Resources\tools\PostBuild.ps1', '$(ProjectDir)', '$(TargetDir)'"
)

The variable $(ConfigurationName) is your friend here.

 

Tip 4: Zipping your build with a password

It is easy to zip your build output but if you want to secure it with a password the standard .NET classes leave you alone. You can use 7zip from the commandline (download the Portable Apps version) and zipping is very easy:

$ZipTool = Join-Path -path $ProjectDir -childpath "Resources\Tools\7z.exe"

function create-zip([String] $zipTool, [String] $aDirectory, [String] $aZipfile){
[Array]$arguments = "a", "-tzip", "$aZipfile", "$aDirectory", "-pSECUREPWD","-r"
& $zipTool $arguments
}

$Name = "zipped_build.zip"
$zfn = [System.Environment]::ExpandEnvironmentVariables("%temp%\$Name")
create-zip $ZipTool (Join-Path -Path $OutPath -ChildPath "*.*") $zfn

Please notice the $zfn variable: we zip the output in the temp-directory (where we have write-access) using an Environment Variable, which needs to be expanded before. The 7z.exe is located inside the $(ProjectDir) - so every developer has the tool on his machine (and since its the Portable App version, no installation is needed)

Also take notice of the zip password - you might pass this into the script and not hardcode this one here.

Remark: There is also a .NET Wrapper for 7zip, but i have not tried this one out.

 

Tip 5: Working with build version information

You might need to work with version information from the build DLLs: Major, Minor, Build and Revision. This is also easily to achieve with PowerShell using the Reflection-Namespace from .NET:

$file = "yourApp.exe"
$ass = [System.Reflection.Assembly]::LoadFile($file)
$v = $ass.GetName().Version;
$version = [string]::Format("{0}.{1:00}.{2:00}.{3}",$v.Major, $v.Minor, $v.Build, $v.Revision)

$v gives you access to all this version numbers - in this case it is used to create a special version that has leading 0 for the Minor and Build number if needed.

 

Tip 6: Replacing content in a file

For replacing some variables in a template, PowerShell again is your friend:

function replace-file-content([String] $path, [String] $replace, [String] $replaceWith)
{
(Get-Content $path) | 
Foreach-Object {$_ -replace $replace,$replaceWith}| 
Out-File $path
}

This function replaces text and is called like this:

replace-file-content $ChangeLogOutPath "{Major}" $v.Major.ToString()

 

Bonus Tip 7: Uploading the build via FTP including a PowerShell progress bar

$ftpWebRequest = [System.Net.FtpWebRequest]::Create((New-Object System.Uri("ftp://your_ftp_server")))
$ftpWebRequest.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile

$inputStream = [System.IO.File]::OpenRead($filePath)
$outputStream = $ftpWebRequest.GetRequestStream()

[byte[]]$buffer = New-Object byte[] 131072;
$totalReadBytesCount = 0;
$readBytesCount;

while (($readBytesCount = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0)
{
$outputStream.Write($buffer, 0, $readBytesCount)
$totalReadBytesCount += $readBytesCount
$progress = [int]($totalReadBytesCount * 100.0 / $inputStream.Length)
Write-Progress -Activity "Uploading file..." -PercentComplete $progress -CurrentOperation "$progress% complete" -Status "Please wait."
}
$outputStream.Close();
$outputStream = $null;
Write-Progress -Activity "Uploading file..." -Completed -Status "Done!"

 

First, we create a FtpWebRequest object, instruct it to Upload a file. We loop through the $inputStream.Read(...), write to the outputStream and update the $progress variable which is used in the PowerShell Write-Progress cmdlet to update the "UI". Handy if your upload takes some time. You might want to adapt the size of the byte-buffer accordingly to your file-size and upload-speed.

Posted on April 16, 2014 .