Last post I went through the basics of what you do to set up your project on CodePlex along with how to hook into the source control. This blog will explain what I did for my build script. Specifically, how I handled versioning and packaging.
Most developers when confronted with a mundane task that is continually repeated will be compelled to automate it. We are programmers after all, we enjoy making the computer making our lives easier. One of these tasks is the creation of a software package to hand off to someone. Over the years there has been numerous ways to do this, from bat files issuing dos commands, to Apache Ant, and more recently MSBuild. Most open source shops seem to gravitate towards Ant or its .NET couterpart Nant due to its open source roots. Seeing that I am one who spends most of my development life in Visual Studio 2008, the obvious choice to me is MSBuild. I say this because this is what Visual Studio uses under the covers as your vbproj/csproj files are actually MSBuild scripts. Quite a people never know this because it is not obvious how to open them up to edit.
MSBuild Basics
Of course you could open up your project file with some other text editor other than Visual Studio, but what fun would that be, as you would be doing without any Intellisense (yes, I am spoiled by this feature). So in order to open your project script up in visual studio, you must first have a solution defined, then you simply right-click on your project in your solution and choose Unload Project. With your project now grayed out, right-click again and choose Edit. You will now see the MSBuild script. Seeing that this process of unloading our project is a bit annoying, I choose to not edit this file very much. Instead I suggest simply specifying in the file that it should import another file in our project. To see another example of this, search the file for either one of these references
<Import Project="$(MSBuildBinPath)\Microsoft.VisualBasic.targets" />
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
A couple concepts you need to know is that variables within MSBuild are defined with a $(VariableName) token. There are also predefined variables/properties like MSBuildBinPath that are given to us. So the first thing that we are going to do is add a line right under this Import that we found.
<Import Project="$(MSBuildProjectDirectory)\build.targets" Condition="Exists('$(MSBuildProjectDirectory)\build.targets)" />
You will notice we used another attribute of the Import element called Condition. This allows us to open the project even when our additional build script has not been defined. We are done with this script, as I think it unwise to tamper too much with it, as Visual Studio may stomp on us at some point. So now all you need to do is right-click on the project and choose Reload Project.
It is now time to create our build.targets file. Simply do a file add new item and choose an XML Item with the file name of build.targets. The shell of our script needs to contain a MSBuild root element.
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>
MSBuild Properties and Targets
Earlier I showed you how to use pre-existing properties. It is now time to learn how to define our own. This is done via a Property Group.
<PropertyGroup>
<MyVariable>DotNetNuke Rocks</MyVariable>
</PropertyGroup>
Targets in MSBuild can be thought of much in the same way as methods to a programmer. One cool thing about MSBuild is that you can even "override" targets. Probably the two most common targets overridden are BeforeBuild and AfterBuild. To do this, add the following to your script.
<Target Name="BeforeBuild">
<Message Text="$(MyVariable) and so does MSBuild" />
</Target>
Before you compile and get disappointed, you need to know about another shortcoming in Visual Studio. It does not automatically grab your changes. You need to unload and reload your project to have your changes take effect. Also note that if you have a syntax error you will not be able to reload your project. In this case you can either try and update your mistake in notepad, or simply rename your file as our Import earlier will gracefully not error out when our file does not exist.
Now if you do a build you should see a message in your Build Output window.
Task Items and Calling Targets
Two more basic things to cover before we tackle our versioning and packaging. Another common task for our script is to obtain a list of files to act upon. In MSBuild this is done through the creation of Task Items.
<CreateItem Include="$(TargetDir)*.dll;$(MSBuildProjectDirectory)*.ascx;">
<Output TaskParameter="Include" ItemName="InstallZipFiles" />
</CreateItem>
The only difference in accessing this "variable" is that we use the @ sign: @(InstallZipFiles).
Finally, it will help you to organize your script into your own targets. In order to do this you need to know how to define and call a target.
<Target Name="BeforeBuild">
<CallTarget Targets="SetVersionInfo" />
</Target>
<Target Name="AfterBuild">
<CallTarget Targets="ZipFiles" />
</Target>
<Target Name="SetVersionInfo">
<!-- Versioning logic here -->
</Target>
<Target Name="ZipFiles">
<!-- Packaging logic here -->
</Target>
Using Third Party Tasks
One of the unfortunate things about MSBuild is that there is not a lot of Tasks that come with it. Simple things like Copying files it can do, but zipping, versioning, and updating files are missing. Luckily there are many community efforts out there that fill in these gaps. The one that I choose to start with is the MSBuild Community Tasks. These tasks cover (or at least get you started) most of what you wish to automate. Once installed, you can simply import the tasks into your script from their global location. Think of this like adding a reference in your .NET project.
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"
Condition="Exists('$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets')" />
However, what I find most helpful is to create a folder one level up from my project folder that contains all my MSBuild tasks and import the targets file from there. I can then easily add new tasks and import them all from a central location that I control and may be specific to my project.
Reading through the community task help file you will have all the information you need to version your project. Here is the script my CodeEndeavor Templates currently use, though I am considering making my own version soon.
<!-- Obtain Version information from version.txt -->
<Version BuildType="Automatic" RevisionType="None" VersionFile="version.txt" StartDate="12/1/2008">
<Output TaskParameter="Major" PropertyName="Major" />
<Output TaskParameter="Minor" PropertyName="Minor" />
<Output TaskParameter="Build" PropertyName="Build" />
<Output TaskParameter="Revision" PropertyName="Revision" />
</Version>
<!-- DNN requires single digits to be prefixed with a zero -->
<CreateProperty Value="0$(Major)" Condition="$(Major) <= 9" >
<Output TaskParameter="Value" PropertyName="Major" />
</CreateProperty>
<CreateProperty Value="0$(Minor)" Condition="$(Minor) <= 9" >
<Output TaskParameter="Value" PropertyName="Minor" />
</CreateProperty>
<CreateProperty Value="0$(Build)" Condition="$(Build) <= 9" >
<Output TaskParameter="Value" PropertyName="Build" />
</CreateProperty>
<CreateProperty Value="0$(Revision)" Condition="$(Revision) <= 9" >
<Output TaskParameter="Value" PropertyName="Revision" />
</CreateProperty>
<!-- Write new version to assemblyinfo.cs -->
<FileUpdate Files="@(AssemblyInfoFiles)" Encoding="ASCII" Regex="AssemblyVersion\(".*"\)\>"
ReplacementText="AssemblyVersion("$(Major).$(Minor).$(Build).$(Revision)")>" />
<FileUpdate Files="@(dnnFile)" Regex="<version>.*</version>"
ReplacementText="<version>$(Major).$(Minor).$(Build)</version>" />
The final thing we need to do is automatically create a zip package for our project. We will use the Task Items we covered earlier for this task.
<Zip Files="@(InstallZipFiles)" WorkingDirectory="$(MSBuildProjectDirectory)\"
ZipFileName="$(DeployDir)\$(YourVariableForProject).install.v$(Major).$(Minor).$(Build).$(Revision).zip" />
To see the full scripts for the concepts discussed in this entry, grab the source from either the DotNetNuke ClientAPI or DotNetNuke WebControls.