You want to run some of your business logic during your TFBuild process. Maybe before, or maybe after Team Foundation Build (TFBuild) compiles or tests your code. For example, before you compile, you want to set the version of your assembly files. Or after you compile, you want to control the build outputs that get dropped to your staging location.
Until recently, you would have to customize your build process. Plenty has been written about how to develop a custom build process. I recently released a topic that I hope makes getting started on this kind of effort a bit easier: Create a custom build process. Regardless, developing and using custom build process activities of any complexity can be tricky and requires a learning curve.
What if you just want to run a simple batch (.bat) file? Or maybe you need a bit more power and you want to run a PowerShell script, perhaps to leverage even a bit of .Net code. Well, I'm here to share some good news: with Visual Studio and Team Foundation Server 2013 you can do it! In fact, to help you kick off your adventure in TFBuild scripting, following are a few examples you can literally copy, paste, and check in to your team project and use today.
As you can see, the following build definition is based on the default template; no customizations up my sleeve:
Before you begin using these scripts, a few bits about how they work:
- ApplyVersionToAssemblies
- The Build number format is set to provide the script with the version data it needs in 0.0.0.0 form, as shown above.
- The script applies the version data to your AssemblyInfo files so that it is stamped on the binaries when they are compiled.
- GatherItemsForDrop
- Output location is set to AsConfigured so that the script controls what output files are dropped later in the build process.
- After the code is compiled, the script copies binaries from the source folder to the binaries folder so that these files can later be dropped to your staging location.
To use these scripts, simply copy the code below into .ps1 files, check them into your codebase, and then reference them from your build process as shown above. Also make sure to include the folder that contains your script on the build definition Workspace tab.
Run a script before building: version your assemblies
While there are many tasks you might want to perform before MSBuild compiles your code, one of the most common of these might be to apply a version to your assemblies. Jim Lamb wrote a canonical blog post on build process customization: Create a Custom WF Activity to Sync Version and Build Numbers. I patterned the following script using his approach to assembly versioning.
# ApplyVersionToAssemblies.ps1 # # Look for a 0.0.0.0 pattern in the build number. # If found use it to version the assemblies. # # For example, if the 'Build number format' build process parameter # $(BuildDefinitionName)_$(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r) # then your build numbers come out like this: # "Build HelloWorld_2013.07.19.1" # This script would then apply version 2013.07.19.1 to your assemblies. # Enable -Verbose option [CmdletBinding()] # Disable parameter # Convenience option so you can debug this script or disable it in # your build definition without having to remove it from # the 'Post-build script path' build process parameter. param([switch]$Disable) if ($PSBoundParameters.ContainsKey('Disable')) { Write-Verbose "Script disabled; no actions will be taken on the files." } # Regular expression pattern to find the version in the build number # and then apply it to the assemblies $VersionRegex = "\d+\.\d+\.\d+\.\d+" # If this script is not running on a build server, remind user to # set environment variables so that this script can be debugged if(-not $Env:TF_BUILD -and -not ($Env:TF_BUILD_SOURCESDIRECTORY -and $Env:TF_BUILD_BUILDNUMBER)) { Write-Error "You must set the following environment variables Write-Error "to test this script interactively." Write-Host '$Env:TF_BUILD_SOURCESDIRECTORY - For example, enter something like:' Write-Host '$Env:TF_BUILD_SOURCESDIRECTORY = "C:\code\FabrikamTFVC\HelloWorld"' Write-Host '$Env:TF_BUILD_BUILDNUMBER - For example, enter something like:' Write-Host '$Env:TF_BUILD_BUILDNUMBER = "Build HelloWorld_0000.00.00.0"' exit 1 } # Make sure path to source code directory is available if (-not $Env:TF_BUILD_SOURCESDIRECTORY) { Write-Error ("TF_BUILD_SOURCESDIRECTORY environment variable is missing.") exit 1 } elseif (-not (Test-Path $Env:TF_BUILD_SOURCESDIRECTORY)) { Write-Error "TF_BUILD_SOURCESDIRECTORY does not exist: $Env:TF_BUILD_SOURCESDIRECTORY" exit 1 } Write-Verbose "TF_BUILD_SOURCESDIRECTORY: $Env:TF_BUILD_SOURCESDIRECTORY" # Make sure there is a build number if (-not $Env:TF_BUILD_BUILDNUMBER) { Write-Error ("TF_BUILD_BUILDNUMBER environment variable is missing.") exit 1 } Write-Verbose "TF_BUILD_BUILDNUMBER: $Env:TF_BUILD_BUILDNUMBER" # Get and validate the the version data $VersionData = [regex]::matches($Env:TF_BUILD_BUILDNUMBER,$VersionRegex) switch($VersionData.Count) { 0 { Write-Error "Could not find version number data in TF_BUILD_BUILDNUMBER." exit 1 } 1 {} default { Write-Warning "Found more than instance of version data in TF_BUILD_BUILDNUMBER." Write-Warning "Will assume first instnace is version." } } $NewVersion = $VersionData[0] Write-Verbose "Version: $NewVersion" # Apply the version to the assembly property files $files = gci $Env:TF_BUILD_SOURCESDIRECTORY -recurse -include *Properties* | ?{ $_.PSIsContainer } | foreach { gci -Path $_.FullName -Recurse -include AssemblyInfo.* } if($files) { Write-Verbose "Will apply $NewVersion to $files.count files." foreach ($file in $files) { if(-not $Disable) { $filecontent = Get-Content($file) attrib $file -r $filecontent -replace $VersionRegex, $NewVersion | Out-File $file Write-Verbose "$file.FullName - version applied" } } } else { Write-Warning "Found no files." }
Run a script after building: prepare your binaries to be staged
When you set Output location to AsConfigured, TFBuild yields control of which binaries are dropped to you. The binaries are compiled and left in the source directory, organized essentially the same way as if you used Visual Studio to compile them locally on your dev machine. The following script gathers some of the typical binary types from the typical locations and copies them to the folder from which TFBuild copies and drops to your staging location.
# GatherItemsForDrop.ps1 # # Copy the binaries to the bin directory # so that the build server can drop them # to the staging location specified on the Build Defaults tab # # See # http://msdn.microsoft.com/en-us/library/bb778394(v=vs.120).aspx # http://msdn.microsoft.com/en-us/library/dd647547(v=vs.120).aspx#scripts # Enable -Verbose option [CmdletBinding()] # Disable parameter # Convenience option so you can debug this script or disable it in # your build definition without having to remove it from # the 'Post-build script path' build process parameter. param([switch]$Disable) if ($PSBoundParameters.ContainsKey('Disable')) { Write-Verbose "Script disabled; no actions will be taken on the files." } # This script copies the basic file types for managed code projects. # You can change this list to meet your needs. $FileTypes = $("*.exe","*.dll","*.exe.config","*.pdb") # Specify the sub-folders to include $SourceSubFolders = $("*bin*","*obj*") # If this script is not running on a build server, remind user to # set environment variables so that this script can be debugged if(-not $Env:TF_BUILD -and -not ($Env:TF_BUILD_SOURCESDIRECTORY -and $Env:TF_BUILD_BINARIESDIRECTORY)) { Write-Error "You must set the following environment variables" Write-Error "to test this script interactively." Write-Host '$Env:TF_BUILD_SOURCESDIRECTORY - For example, enter something like:' Write-Host '$Env:TF_BUILD_SOURCESDIRECTORY = "C:\code\FabrikamTFVC\HelloWorld"' Write-Host '$Env:TF_BUILD_BINARIESDIRECTORY - For example, enter something like:' Write-Host '$Env:TF_BUILD_BINARIESDIRECTORY = "C:\code\bin"' exit 1 } # Make sure path to source code directory is available if (-not $Env:TF_BUILD_SOURCESDIRECTORY) { Write-Error ("TF_BUILD_SOURCESDIRECTORY environment variable is missing.") exit 1 } elseif (-not (Test-Path $Env:TF_BUILD_SOURCESDIRECTORY)) { Write-Error "TF_BUILD_SOURCESDIRECTORY does not exist: $Env:TF_BUILD_SOURCESDIRECTORY" exit 1 } Write-Verbose "TF_BUILD_SOURCESDIRECTORY: $Env:TF_BUILD_SOURCESDIRECTORY" # Make sure path to binary output directory is available if (-not $Env:TF_BUILD_BINARIESDIRECTORY) { Write-Error ("TF_BUILD_BINARIESDIRECTORY environment variable is missing.") exit 1 } if ([IO.File]::Exists($Env:TF_BUILD_BINARIESDIRECTORY)) { Write-Error "Cannot create output directory." Write-Error "File with name $Env:TF_BUILD_BINARIESDIRECTORY already exists." exit 1 } Write-Verbose "TF_BUILD_BINARIESDIRECTORY: $Env:TF_BUILD_BINARIESDIRECTORY" # Tell user what script is about to do Write-Verbose "Will look for and then gather " Write-Verbose "$FileTypes files from" Write-Verbose "$Env:TF_BUILD_SOURCESDIRECTORY and copy them to " Write-Verbose $Env:TF_BUILD_BINARIESDIRECTORY # Find the files $files = gci $Env:TF_BUILD_SOURCESDIRECTORY -recurse -include $SourceSubFolders | ?{ $_.PSIsContainer } | foreach { gci -Path $_.FullName -Recurse -include $FileTypes } if($files) { Write-Verbose "Found $files.count files:" foreach ($file in $files) { Write-Verbose $file.FullName } } else { Write-Warning "Found no files." } # If binary output directory exists, make sure it is empty # If it does not exist, create one # (this happens when 'Clean workspace' build process parameter is set to True) if ([IO.Directory]::Exists($Env:TF_BUILD_BINARIESDIRECTORY)) { $DeletePath = $Env:TF_BUILD_BINARIESDIRECTORY + "\*" Write-Verbose "$Env:TF_BUILD_BINARIESDIRECTORY exists." if(-not $Disable) { Write-Verbose "Ready to delete $DeletePath" Remove-Item $DeletePath -recurse Write-Verbose "Files deleted." } } else { Write-Verbose "$Env:TF_BUILD_BINARIESDIRECTORY does not exist." if(-not $Disable) { Write-Verbose "Ready to create it." [IO.Directory]::CreateDirectory($Env:TF_BUILD_BINARIESDIRECTORY) | Out-Null Write-Verbose "Directory created." } } # Copy the binaries Write-Verbose "Ready to copy files." if(-not $Disable) { foreach ($file in $files) { Copy $file $Env:TF_BUILD_BINARIESDIRECTORY } Write-Verbose "Files copied." }
A few final bits
You'll notice that these scripts leverage some environment variables to get the key bits of data they need to perform their functions. I've cataloged these and all the other environment variables that are available to your scripts: Run your scripts in a default build process. (Note: there is a known bug with the TF_BUILD_TESTRESULTSDIRECTORY not being correctly handled by TFS 2013 Preview. This bug will be fixed in the next release.)
To debug the script while logged on to your dev machine or on the build server, you can manually set the TF_BUILD environment variables in your PowerShell console session. In fact, you'll notice the scripts automatically prompts you to do so. Use the -verbose parameter if you want to get details about what the script is doing and view them from the diagnostic log. Specify -disable if you want to disable any changes the script might make while leaving it referenced by your build process.
I invite you to post your comments here. For questions, I encourage you to post them on our forums: http://social.msdn.microsoft.com/forums/en-us/home.