We are starting to automate and standardize our build process. I think the idea that F5 is not a a build strategy is finally surfacing as a real problem. The first step in my mind is to come to some convention on the structure. Doing so would make configuration a lot simpler. We can default the convention and you can overwrite if you feel the need.
Microsoft has some guidance on the subject of how to structure the source when using TFS. I’m not really sure how the choice of source control factors into that decision, but we’ll just go with it. The structure I’m playing with now is very similar to their recommendation.
/Fooberry/
1.0.0/ <-- the build file goes here
docs/
source/
application/ <-- the source for the ui
application.tests/ <-- the UI tests
core/
core.tests/
...I'm not settled on how this level
will look. It isn't important for
now. I'm more concerned with the
files needed to build.
libs/
my.company.build/
msbuild.community.tasks/
nunit/
nxslt/
nunit4teambuild/
n*...all shared libs and tools
...maybe some other dirs here eventually
main/ <-- the trunk copy just like the 1.0.0 tree
/SomeOtherApp/
The main point here is that all the tools we use are inside the source control for every app and every branch of that app. It may seem like a waste of disk space, but what it allows is all configuration information to be relative to the build root. A specific branch can upgrade to a new NUnit and all the other builds don’t need to worry about backwards computability.
A folder that may look a little odd is my.company.build. I would like this folder to hold as much of the automated build extensions as possible. There are going to be a few additional steps that we need to add to the out of the box TFSBuild.proj. The plan is to put all the new targets in a my.company.build.targets file along with any custom tasks that we need to build. Once we have that, we can stick the following line into the TFSBuild.proj and we snap on all our custom steps.
<Import Project="$(SolutionRoot)\libs\my.company\my.company.build.targets"/>
Getting the import setup was a little bit of pain. Running locally, $(SolutionRoot) doesn’t map to the correct path. The path to the targets file was mapping someplace crazy. This has a really bad smell, but I was overjoyed that it worked remotely, so I didn’t care too much for now. It’s only day one. We will need to build locally eventually. That’s on the TODO list already.
I have a bit of a confession. I wish that was the manner in which I started this process, but I didn’t realize that we could externalize the build steps to my.company.build until after I started filling out the steps. This is is how, in my opinion, frameworks are most successfully built. We extract the code that could be made common instead of head out to write something that will solve everyone’s problem, even though you don’t know what their problem might be.
So to start on the first build step; running NUnit tests. After a quick Google search,
Nunit4TeamBuild on CodePlex outlined the steps we needed to take.
- Run Nunit-console and produce an XML log file
- Convert the Nunit XML output to an MSTest test results file (with a .trx extension)
-
- Use MSTest /publish to push merge the trx file with an existing build
Sounds pretty straight forward. There are a few more dependancies. It uses the MSBuild Community Tasks and nxslt2 (There is a new nxslt3 which uses .Net 3.5…not sure what that gains us though).
The documentation for Nunit4TeamBuild recommends adding an AfterCompile target to execute the NUnit tests. We can put this in our my.company.build.targets file. There are just a few housekeeping items first. The example proj file has hard coded paths to the MSBuild Commnuity Tasks, NUnit and NXslt. To make those relative, assign the….
<PropertyGroup>
<MSBuildCommunityTasksPath>$(SolutionRoot)\libs\community.msbuild.tasks\</MSBuildCommunityTasksPath>
<NUnitPath>$(SolutionRoot)\libs\nunit\</NUnitPath>
</PropertyGroup>
<Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets"/>
When we go to invoke the NUnit task, we need to be sure and give the ToolPath of $(NUnitPath).
<NUnit ContinueOnError = "true'
Assemblies = "@(TestAssemblies)"
OutputXmlFile = "$(OutDir)nunit_results.xml"
ToolPath = "$(NUnitPath)">
<Output TaskParameter = "ExitCode"
PropertyName = "NUnitResult" />
</NUnit>
*Note: I’m playing with the formatting of the XML. I’m trying out this style for now.
So that takes care of the NUnit and the MSBuild Community Tasks. The next tool that needs a relative path is NXslt. The example project shows invoking the command line executable via the Exec task.
<Exec Command="C:\path\to\nxslt\nxslt2.exe "$(OutDir)nunit_results.xml" "$(SolutionRoot)\nunit transform.xslt" -o "$(OutDir)nunit_results.trx""/>
While that works, NXslt comes bundled with an MSBuild task that can replace it with something more elegant. To use the task, add the Using
<UsingTask AssemblyFile = "$(NXsltTaskAssembly)"
TaskName = "Nxslt" />
And replace that Exec with the following:
<Nxslt In = "$(OutDir)nunit_results.xml"
Style = "$(SolutionRoot)\libs\nunit4teambuild\nunit transform.xslt"
Out = "$(OutDir)nunit_results.trx" />
One more to go. MSTest.exe is invoked to publish the newly formatted results. This one might be more difficult to keep relative. Right now, this is pointing (or trying to point) to the MSTest.exe under Program Files. With all that said, I’m unable to find where MSTest is on the build server. Having no access to it makes it a little difficult. Unfortunately I cannot guarantee that the final step will actually work. I hope to have more news to report next week.