This is cache of http://pluralsight.com/community/blogs/keith/archive/2008/03/07/50388.aspx. Cache is the snapshot of article that we took when we index feed.
To see original page click here.
We are not affiliated with the authors of this article and not responsible for its content.
Enabling hierarchical nant builds
2008-03-07 12:49:00 by keith-brown in Security Briefs
 

In a recent post, I talked about my experience enabling continuous integration for the internal builds here at Pluralsight. I recently worked with Craig to restructure our nant build. As part of that, I wanted to ensure that I could run the build from anywhere in the source tree. We use a typical hierarchical build where each project has a build script that knows how to compile, test, deploy, etc. based on the specified target. Then at the top of the tree, there's a build script that runs all the other ones. That root build script is what gets run automatically by Cruise Control.

My root script defines a bunch of properties, like where the output directories for the overall build live, where the tools live, and so on. And that works fine when I run the build from the root. The properties get defined, all the child scripts are run with <nant/> tasks, and they see those properties. But if I want to drill down into the tree and run one of the build scripts lower down, suddenly there's problems because it depends on properties that are only defined in the root script. I really like being able to run builds from anywhere in the tree for perf - if I'm trying to fix a particular project, I don't necessarily want to wait for unit tests on the entire tree to run in order to see if mine passed.

Craig made a great suggestion. Put the properties into a separate script (we named it properties.nant) and <include/> that script. Then to enable hierarchical builds, we'd create a properties.nant file for each folder in the tree, which would <include/> its parent. That way I could define properties anywhere in the tree, and they would be "inherited" by anything below it.

I took that idea one step further, because I didn't want to maintain a bunch of property scripts with nothing in them but an <include/> for the parent. I wrote an <includefromparent/> nant task that walks up the directory tree looking for the target file. So now I can do this:

<includefromparent buildfile="properties.nant"/>

This worked great! But now I ran into a problem. Many of my properties are defined like so in the root properties.nant file:

<property name="libraryOutputDir" value="${project::get-base-directory()}\artifacts\libraries"/>

Do you see the issue? If I run the build with the root script, everything works fine, because it's the root nant project I'm building, and get-base-directory() refers to the root of the project, where the artifacts folder lives. But if I run from lower in the tree, it's a different project, and get-base-directory() refers to a subfolder, where the artifacts folder definitely should NOT be.

I needed a way to find the root of the project tree. So I build a second really simple nant task:

<findmarkeddir markerfile="filetolookfor.txt" property="root"/>

This task simply looks up the directory hierarchy until it finds the specified marker file, then puts the name of that directory (the "marked" directory) into a designated property (here I've called it root). With that in place, I rewrote my property definitions in terms of the base directory:

<property name="libraryOutputDir" value="${root}\artifacts\libraries"/>

VoilĂ ! I can now run builds from any of my build scripts. They inherit properties hierarchically like you'd expect, and the system is quite easy to maintain. If you'd like to use these tasks, I've included the code for them below (not much code, really). And if you've never written a nant task yourself before, here's the article I used to figure out how it's done (it's super easy). Here's what you should read to learn about the various options for deploying your custom task assembly so nant recognizes it.

Enjoy!

Here is FindMarkedDirTask.cs

using System;
using System.Collections.Generic;
using System.Text;
using NAnt.Core;
using NAnt.Core.Attributes;
using System.IO;

namespace PluralsightNantTasks {

[TaskName("findmarkeddir")]
public class FindMarkedDirTask : Task {

  [TaskAttribute("markerfile", Required = true)]
  [StringValidator(AllowEmpty = false)]
  public string MarkerFileName { get; set; }

  [TaskAttribute("property", Required = true)]
  [StringValidator(AllowEmpty = false)]
  public string PropertyName { get; set; }

  protected override void ExecuteTask() {
    string searchDir = this.Project.BaseDirectory;
    do {
      if (MarkerFileExistsIn(searchDir)) {
        this.Project.Properties[PropertyName] = searchDir;
        return;
      }
      searchDir = ParentOf(searchDir);
    } while (!IsRootDirectory(searchDir));
  }

  private bool IsRootDirectory(string path) {
    return Path.GetPathRoot(path) == Path.GetFullPath(path);
  }

  private string ParentOf(string directory) {
    return Path.GetFullPath(Path.Combine(directory, ".."));
  }

  private bool MarkerFileExistsIn(string directory) {
    return File.Exists(Path.Combine(directory, MarkerFileName));
  }
}
}

And here is IncludeFromParentTask.cs (note I derive from the built-in include task):

using System;
using System.Collections.Generic;
using System.Text;
using NAnt.Core;
using NAnt.Core.Attributes;
using NAnt.Core.Tasks;
using System.IO;
using System.Globalization;

namespace PluralsightNantTasks {

[TaskName("includefromparent")]
public class IncludeFromParentTask : IncludeTask {

  protected override void Initialize() {
    string fileName = BuildFileName;
    if (fileName.Contains("/") || fileName.Contains(@"\\"))
      throw new BuildException(string.Format(
        CultureInfo.CurrentCulture,
        "buildfile attribute must only be a filename"));

    string relativePathToFoundFile = SearchParentDirectory(
      Project.BaseDirectory, fileName, 0);
      
    if (null == relativePathToFoundFile)
      throw new BuildException(string.Format(
        CultureInfo.CurrentCulture,
        "Couldn't find a file named {0}" +
        " in a parent directory of {1}",
        fileName, Project.BaseDirectory));

    // have to use a relative path here
    // because  task uses
    // Path.Combine(projectDir, BuildFileName)
    // to get the full path
    BuildFileName = relativePathToFoundFile;

    base.Initialize();
  }

  private string SearchParentDirectory(string directory,
                        string fileName, int searchDepth) {
    ++searchDepth;
  
    // see if we've traversed all the way to the root
    string currentPath = Path.GetFullPath(directory);
    if (currentPath == Path.GetPathRoot(currentPath))
      return null;

    // recurse until we find the file
    string parentDir = Path.GetFullPath(
      Path.Combine(currentPath, ".."));
    string path = Path.Combine(parentDir, fileName);
    if (File.Exists(path)) {
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < searchDepth; ++i)
        sb.Append(@"..\");
      return Path.Combine(sb.ToString(), fileName);
    }
    else return SearchParentDirectory(parentDir,
                          fileName, searchDepth);
  }
}
}
 
 
 
 
 
 
TOP SEARCH
Expand / MinimizeClose Widget
  •  
RECENT SEARCH
Expand / Minimize
  •  
RELATED VIDEO
Expand / Minimize
SecurityRatty FAQ
Sergey Zarubin, 31yo
CISSP, CCSP
Moscow, Russia