July 2005 - Posts

I received another question about NAnt, asking what Targets were, and for direction in building a custom MSI package. Targets are a fairly simple concept so I'll tackle that first. As far as building MSI's, I can offer an example of how I do it, and pointers to where you can find additional information. Here goes!

 

Targets

Targets are basically a way of grouping tasks in way that can be called from elsewhere. You can call a target from elsewhere in your NAnt file, or refer to a specific target when calling NAnt.exe. Typically a NAnt file will contain a target called "build" that contains tasks and/or calls to other targets in your build file. Here is a simplified version of a build target that I use:

 <target name="build" description="Build the application.">
  <call target="clean" />
  <call target="compile" />
  <call target="discsetup" />
  <call target="rununittests" />
  <call target="fxcop" />
 </target>

This target simply calls some other targets that contain tasks to do various things. For example, the "clean" target looks like this:

 <target name="clean" description="Remove all files from the Working directory">
  <delete>
   <fileset basedir="${working.dir}\">
    <include name="**" />
   </fileset>
  </delete>
  <mkdir dir="${working.dir}" />
 </target>

You can see that it executes a couple of tasks. First, the working directory (as in the working.dir property) is removed. Then a new working directory is created. BTW, I think that the current version of NAnt will do this just fine using the dir attribute of the delete task - feel free to improve on it!

 

Building Setup (MSI) Files

There are several ways of creating setup files through NAnt. If you use Visual Studio to configure your setup file (using a Setup Project), you can invoke Visual Studio through NAnt to build the file. If you use a third party application, you would build it in a similar fashion. You can also use a NAnt file with XML tags describing your MSI file in order to generate one, using a feature of NAntContrib.

Visual Studio

First of all, the Visual Studio method. Here's an example of how my NAnt script is set up to handle this:

 <target name="build-msi" description="Build an MSI file for installation of the program">
  <property name="msi-file-name" value="application\TellerSetup\${build.config}\TellerForPOSSE.msi" />
  <delete>
   <fileset basedir="application\TellerSetup\${build.config}">
    <include name="*" />
   </fileset>
  </delete>
  <mkdir dir="${working.dir}\log" />
  <exec failonerror="false" basedir="c:\Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE"
   program="devenv.exe" commandline="${build.solution} /build ${build.config} /project TellerSetup /out ${working.dir}\log\msi.log" />
  <fail if="${not file::exists(msi-file-name)}">The MSI failed to build. See log\msi.log for more details.</fail>
 </target>

First of all, I set a property called "msi-file-name" with the path to the msi file that I am going to build. This is used later on to check that the file exists.

Next, I use a delete task to remove anything that currently exists in the folder where I'll be creating the MSI. This is to clean up any files left over from the last build, so that we can start with a clean slate.

mkdir is used to create the directory that will contain the log, if it doesn't already exist. We will use this to capture a log file of the Visual Studio activity, so that we can troubleshoot it later on if needed.

Finally, the exec task is used to run Visual Studio (devenv.exe) and tell it to build the setup project. There are various command-line options that you can pass into devenv.exe. This demonstrates a few of them. Note that the output from the command is sent to a log file in our log directory.

In order to determine if the MSI was built successfully or not, I simply test to see if the file exists. Ideally I would let the exec task fail if there were problems, but I found that there were valid conditions that caused devenv.exe to set the errorlevel anyways, causing NAnt to fail the build. A fail task that raises a build error if the file doesn't exist did the trick OK - and demonstrates calling functions from expressions as well. Since Visual Studio creates the actual .msi file at the end of the build process, I figured this was pretty safe.

That should do it! Note that you will need Visual Studio installed on your build machine in order for this to work - in my case, I actually have it installed on my build server (which isn't ideal, but hasn't caused any problems for me either). This method seems to work reasonably well=

NAntContrib

NAntContrib is a project that supplements NAnt by providing a bunch of additional tasks. You can download it and access the help from http://nantcontrib.sourceforge.net/. Among other cool things, NAntContrib includes an msi task to build MSI files.

I haven't used this task and don't know how well it works. You can get the documentation for it here. I believe that there is also a tool included in the download that allows you to generate the necessary XML from a Visual Studio Setup project - looks like the slingshot task, though the documentation says that it has been deprecated, so I'm not sure what you're supposed to do now. It would be interesting if you could have NAnt generate the XML from a Setup Project and then run it using MSI, without the need for Visual Studio on the server. Feel free to post comments here if you have more information or decide to try it out.

 

You can find more information about NAnt on the main NAnt page: http://nant.sourceforge.net. There's a simple example build file at http://nant.sourceforge.net/release/latest/help/fundamentals/buildfiles.html. All the best!

Posted by Joshua Langemann | 2 comment(s)
Filed under:

Recently I ran a lab for the Denver Visual Studio User Group on open source tools NAnt, NUnit, Subversion, and Cruise Control.Net, tools that significantly improve the development cycle in .Net. I briefly touched on the NAnt feature allowing C#, VB, or J# code to be included in your NAnt file, and dynamically compiled and run by NAnt during a build. I use this feature to handle versioning of assemblies the way I want the versioning to work. One of the lab participants requested a copy of this code. As always, feel free to provide feedback!

I realize that today's version of NAnt allows an assemblyinfo.cs file to be built dynamically in the script using the <asminfo> task, providing some flexibility in how versioning and signing is handled during the build. When I put this together, either that feature didn't exist or I wasn't aware of it. Thus, my approach involves parsing assemblyinfo.cs files and substituting version numbers at build time - you may be able to improve on this. As I look at it, there are other things I could do to improve and simply it, but I'll just include the currently working copy for now, and let you adapt it to your needs.

My versioning model is based on a control file containing the last-used version number. I increment the third portion of the number during a build, and commit this change to a Subversion repository if the build was successful. This way version numbers are only assigned to successful builds, and I don't waste version numbers on builds that failed. Of course, failed builds never leave the build server.

The first target is getversion, which pulls the portions of the version number out of the version.txt file into properties:

<target name="getversion" description="Populate the version properties based on the version.txt file">
  <echo message="${nant.project.basedir}"/>
  <script language="C#">
   <code><![CDATA[
    public static void ScriptMain(Project project)
    {
     // parse version document to get version information
     string fileName = Path.Combine(project.BaseDirectory, project.Properties["build.version.filename"]);
     StreamReader reader = new StreamReader(fileName);
     string versionInfo = reader.ReadLine();
     reader.Close();
     Regex pattern = new Regex("[0-9]+");
     MatchCollection matches = pattern.Matches(versionInfo);
     if (matches.Count != 4)
      throw new Exception(string.Format("Version number {0} in {1} has incorrect format.", versionInfo, fileName));
     int major = int.Parse(matches[0].Value);
     int minor = int.Parse(matches[1].Value);
     int build = int.Parse(matches[2].Value);
     int revision = int.Parse(matches[3].Value);
     project.Properties.Add("build.version.major", major.ToString());
     project.Properties.Add("build.version.minor", minor.ToString());
     project.Properties.Add("build.version.build", build.ToString());
     project.Properties.Add("build.version.revision", revision.ToString());
    }
   ]]></code>
  </script>
  <call target="setversionstring" />
 </target>
 
 
This calls setversionstring, which simply combines the seperate properties into a single versionstring property:
 
 <target name="setversionstring" description="Set the build.version.versionstring property">
  <script language="C#">
   <code><![CDATA[
    public static void ScriptMain(Project project)
    {
     string versionString = string.Format("{0}.{1}.{2}.{3}",
      project.Properties["build.version.major"],
      project.Properties["build.version.minor"],
      project.Properties["build.version.build"],
      project.Properties["build.version.revision"]
     );
     project.Properties["build.version.versionstring"] = versionString;
     project.Log(Level.Info, versionString);
    }
   ]]></code>
  </script>
 </target>

 
The incrementbuildnumber target is called from server builds (not developer-machine builds) to increment the third section of the number:
 
 <target name="incrementbuildnumber" description="Increment the build number and write to version.txt file">
  <script language="C#">
   <code><![CDATA[
    public static void ScriptMain(Project project)
    {
     string fileName = Path.Combine(project.BaseDirectory, project.Properties["build.version.filename"]);
     int major = int.Parse(project.Properties["build.version.major"]);
     int minor = int.Parse(project.Properties["build.version.minor"]);
     int build = int.Parse(project.Properties["build.version.build"]);
     int revision = int.Parse(project.Properties["build.version.revision"]);
     build++;
     string versionString = string.Format("{0}.{1}.{2}.{3}", major, minor, build, revision);
     project.Properties["build.version.build"] = build.ToString();
     
     StreamWriter writer = new StreamWriter(fileName, false);
     writer.WriteLine(versionString);
     writer.Close();
    }
   ]]></code>
  </script>
  <call target="setversionstring" />
 </target>

 
Finally, setversion is used to update the assemblyinfo.cs files with the updated version number:
 
 <target name="setversion" description="Stamp the version info onto assemblyinfo.cs files">
  <foreach item="File" property="filename">
   <in>
    <items basedir="application">
     <include name="**\AssemblyInfo.cs"></include>
    </items>
   </in>
   <do>
    <script language="C#">
     <code><![CDATA[
     public static void ScriptMain(Project project)
     {
      //FileStream file = File.Open(project.Properties["filename"], FileMode.Open, FileAccess.ReadWrite);
      StreamReader reader = new StreamReader(project.Properties["filename"]);
      string contents = reader.ReadToEnd();
      reader.Close();
      string replacement = string.Format(
       "[assembly: AssemblyVersion(\"{0}.{1}.{2}.{3}\")]",
       project.Properties["build.version.major"],
       project.Properties["build.version.minor"],
       project.Properties["build.version.build"],
       project.Properties["build.version.revision"]
      );
      string newText = Regex.Replace(contents, @"\[assembly: AssemblyVersion\("".*""\)\]", replacement);
      StreamWriter writer = new StreamWriter(project.Properties["filename"], false);
      writer.Write(newText);
      writer.Close();
     }
     ]]>
     </code>
    </script>
   </do>
  </foreach>
 </target>
 
This way, when the project compiles, our version number will be used on each assembly. For developer builds, I hard-code the "build.version.build" to 0, so that I can identify the build as a developer build. This is accomplished with the following task, called after getversion:
 
  <property name="build.version.build" value="0" />
 
I trust that this will be helpful!
 
- Josh
Posted by Joshua Langemann | 5 comment(s)
Filed under:

I just received an anonymous comment to the Blogger version of the Data validation in .Net using ErrorProvider and DataSet errors post, asking for the code to setFocusToErrorOnRow. This method is designed for a ComponentOne TrueDBGrid - my code is in a descendant of the C1TrueDBGrid class and wouldn't apply to a .Net data grid. Even so, here is the code - perhaps it will be useful, and as always, suggestions are welcome!

private bool setFocusToErrorOnRow (DataRow row, int rowIndex)

{

  bool errorProcessed = false;

  if (row.HasErrors)

  {

    DataColumn[] columnsInError = row.GetColumnsInError();

    if (columnsInError.Length > 0)

    {

      int columnIndex = 0;

      // Attempt to locate column in error on grid, so that we can set focus there

      foreach (C1.Win.C1TrueDBGrid.C1DisplayColumn displayColumn in this.Splits[0].DisplayColumns)

      {

        if (displayColumn.DataColumn.DataField == columnsInError[0].ColumnName)

        {

          this.Row = rowIndex;

          this.Col = columnIndex;

          errorProcessed = true;

          break;

        }

        columnIndex++;

      }

 

      // If we couldn't find a column, at least set focus to the row

      if (!errorProcessed)

      {

        this.Row = rowIndex;

        errorProcessed = true;

      }

    }

    else

    {

      this.Row = rowIndex;

      errorProcessed = true;

    }

  }

  return errorProcessed;

}