The weak link in many software projects is the final build. Too often we believe we're done when the last line of code compiles on our machine. But there's still work to be done, tying up all the loose ends so that someone else can extract the code from version control and build it. And just because someone else can extract and build the project doesn't mean it is pleasant or easy. The chore of mapping your Team Foundation Server (TFS) workspace to a location on your disk further adds to the pain of building your code in a clean test environment.
The process of extracting and building the code should be automated to ensure that the process is reproducible. Automation also makes the process faster and easier, though that is secondary.
In this article, we describe a PowerShell script that we have used to automate extracting application source code from Visual Studio Team System and building a Visual Studio .NET solution.
Why not MSBuild/TeamBuild?
So why didn't we just use the MSBuild/TeamBuild tools instead of writing a PowerShell script? We wanted a build tool that was lightweight and easy for developers to use. This script can be used on any machine that has Visual Studio .NET installed, and it doesn't require a Team Build Server to be set up. The developer can watch the build happen in real time. We also wanted to experiment with PowerShell. We have used this script as part of our deployment process for ASP.NET applications for over a year and it has served us well.
If you do not have PowerShell installed, download it from Microsoft.
Before running any PowerShell script, you must set the execution policy. For example, entering
set-executionpolicy remoteSigned from the PowerShell prompt will allow local scripts such as this one to run unsigned. This only needs to be done once. Subsequently, any PowerShell script can be run on that machine.
You might also enjoy the PowerShell tools from the PowerShell Community Extensions (PSCX) available here. This library adds many worthwhile tools to your PowerShell installation. One simple but useful addition is the ability to right-click on a folder in Windows Explorer and select "Open PowerShell Here."
PowerShell requires an explicit path to scripts. This security feature prevents someone from running a malicious script with the same name as a familiar script but with a different location. This means that you cannot run a script foo.ps1 from your current directory simply by typing its name. You must specify .\foo.ps1.
Using the Code
What the Script Does
The PowerShell script BuildMe.ps1 takes a small XML configuration file as input. We will call this file BuildMe.xml here, but it can be named anything you like.
The PowerShell script:
- Reads your input XML file containing data about the TFS project you want to build
- Remembers any currently-mapped drive letters
- Maps any new required drives (as specified in the input file); these are available during the build
- Creates a temporary workspace in the current folder location
- Extracts the source code (described in the input file) from the TFS source repository
- Builds the specified solution/configurations, using the version of Visual Studio .NET described in the solution file
- Deletes the temporary workspace
- Restores any drive mappings that were present before the script started.
How to Run the Script
The input file can be specified to the script in three ways:
- If no argument is given, the script will look for a file named BuildMe.xml in the current directory.
- If your input file has a different name, or is in a different location, you may specify the file name and file system location as a command-line argument.
- You may specify the TFS path to the input file.
To run the script, do the following:
- Edit the BuildMe.ps1 file to supply the name of your TFS server machine. At line 126 in the file, change the text that looks like this
$g_myServer = "theTFSserverName" to supply your server name.
- Navigate to a work folder, such as D:\buildarea\. The BuildMe.ps1 must be accessible from this folder.
- If you have installed PSCX, right-click on the folder and select "Open PowerShell Here" from the Context menu. Otherwise, open a PowerShell window first and navigate to the work folder.
- In the resulting CommandWindow type .\BuildMe.ps1 or specify the full path to the BuildMe.ps1 file. This will cause the input to be read from the default file, BuildMe.xml, in the current directory. The input file can have any valid filename.
- Alternatively, you can specify the location of the input XML file in TFS: .\BuildMe.ps1 $/MyTFSproject/BuildMe.xml.
$/ at the beginning of the path identifies the XML file as being in TFS. This is most convenient as it allows you to not have to first extract the input XML file from TFS; the script performs the "get" from version control.
The output from the Visual Studio .NET compiler is written to BuildMe.xml.SolutionBuild.log if the script runs with the default input file in the working directory. If a different input file name is specified, the log file name will reflect the name of the input file with the SolutionBuild.log
string appended to it. This log file is simply the log file generated by Visual Studio as it builds the solution.
NB: You can only have one TFS workspace at a time mapped to a directory location. If you try to build a second project in a location mapped to the workspace of another project, the script will print out a warning saying that
CreateMapping failed because the path is already mapped to a workspace.
Preparing the Input File
The project(s) to build are specified in the input file as follows. The example project below is for a fictitious TFS project called
<SlnToBuild> .\BEETLE\BEETLE_sln\BEETLE.sln </SlnToBuild>
<ConfigToBuild> Release </ConfigToBuild>
<Drive> M: </Drive>
<MapTo> \\myServer\myShare\myDir </MapTo>
<ProjName> $/BEETLE </ProjName>
<ProjLocalDir> .\BEETLE\code </ProjLocalDir>
<ProjTreeToGet> $/BEETLE </ProjTreeToGet>
<Label> v0.6 </Label>
<SlnToBuild> is the path (relative to the current directory location) to the Visual Studio .NET solution file that is to be built. This file will be extracted from TFS when the
<TFSProject> element is processed by the script.
<ConfigToBuild> is the name of the Visual Studio .NET configuration that should be built. This element may be repeated if you want to build more than one configuration.
<BuildBitsOutputFolder> is the path (relative to the current directory location) where the results of the build are expected to be produced. This is an "information only" path that is output by the script at the end of a successful build. The path is not verified.
<MappedDrive> element provides a facility for mapping drives that are needed during the build. The
<MapTo> element may specify a network location or a location on your local disk. The
SUBST command is used to map local drive letters. If a drive having the same letter is already mapped, it will be remembered, unmapped, then the specified letter will be mapped. At the conclusion of the build, the specified drive letter will be unmapped and the previous map will be restored. This element may be repeated. This element may be omitted if no mapped drives are desired.
<TFSproject> element specifies information about the TFS Project that will be extracted from the version control system. This element may be repeated.
<ProjName> is the name of the TFS Project. Note the
$/ prefix, which indicates this is a path within the TFS system.
<ProjLocalDir> is the (relative) location on your local disk where the source tree will be placed when it is extracted from TFS.
<ProjTreeToGet> is the location in the TFS Project folder hierarchy where the source code "get" should begin. Note the
$/ prefix, which indicates this is a TFS path. All the code from this point down in the tree will be retrieved from TFS. In the above example, all the code from the TFS location at $/BEETLE will be extracted and placed on the hard disk at the relative location .\BEETLE\code.
<Label> is the label that you applied to the source code that you wish to retrieve. To get the "latest version," just leave the contents of this element blank.
PSH> set-executionPolicy unrestricted
This relaxes the default security setting in PowerShell, allowing your code to run. You need to do this only once per machine. It will remain in effect thereafter.
Runs the PowerShell script. It will read the default input file (BuildMe.xml) in the current directory. The TFS project(s) described in the input file will be extracted from TFS and built at the location described in the input file.
PSH> .\BuildMe.ps1 MyProject.xml
Runs the PowerShell script. It will read the specified input file (
MyProject.xml) in the current directory. The TFS project(s) described in the input file will be extracted from TFS and built at the location described in the input file.
PSH> .\BuildMe.ps1 $/BETTLE/BuildMe.xml
Runs the PowerShell script. It will get the input file, BuildMe.xml, from the $/BEETLE TFS project and place it in the current directory. The input file will then be read and the TFS project(s) described in the input file will be extracted from TFS. The project(s) will then be built at the location described in the input file. NOTE: The
$/ prefix on the input file indicates to the script that the input file is in TFS (and not on the local disk).
PSH> .\BuildMe.ps1 $/BETTLE/BuildMe.xml v1.2
Same as above except the input file having label "v2.1" will be extracted from TFS. The label(s) in the input file will be used to extract the other source code.
Points of Interest
We found the TFS .NET class libraries to be very thorough, but also rather undocumented and difficult to use. The PowerShell code shown here illustrates the use of the TFS API for several interesting operations. Special thanks goes to James Manning whose blog helped us with the TFS API.
- 18th April, 2008: Initial release