Friday 27 March 2009

Horn Architecure Overview

Horn Architecure Overview

This is the third in a series of posts to introduce the horn package management system.  An introduction to horn can be found here with a further explanation of the horn dsl here.

The following text will give an overview of the code structure and architecture that currently exists for horn.

Firstly let us examine the project structure of horn:

Hopefully the project names of the solution are fairly self explanatory.

  • Horn.Core is the chief player of the piece with hopefully the majority of all business logic contained here.

  • Horn.Console is the current command line client used to invoke horn commands.

  • Hron.Core.Spec contains the BDD style unit tests to drive out the functionality of Horn.Core

  • Horn.Core.Integration contains the end to end integration tests used to fully test the horn functionality.

  • Horn.Core.Spec.Framework contains base classes used by both the testing project


Horn is invoked from the command line with a familiar DOS application/swtch instruction set:

horn -install:horn

This install switch is parsed out by a command line parser utility.  This triggers a runtime search for a specific package tree directory node.  In the above example the search will be performed for a directory named horn.

One of the artifacts included in each specific package directory node is the package specific build.boo file containing the install instructions for the requested package.

The philosophy of how commands are invoked is unsurprisingly based on the command design pattern.  We are able to resolve which specific command is required by examining the instructions that are sent via the command line.  In the case of "horn -install:horn", an install command is required.

Our command abstraction is a simple .NET interface with one method contract:

public interface IPackageCommand
{
    void Execute(IPackageTree packageTree, IDictionary<string, IList<string>> switches);
}

The execute method takes as an argument an in memory representation of the package specific tree node.  All nodes of the tree or indeed the tree itself are defined in terms of an interface to allow mocking or stubbing that should encourage futher unit testing. A further IDictionary argument is passed to represent the command line switches.

We use the Castle Windsor Inversion Of Control container to loosely couple our dependencies and also promote testability.  This means we can pull the concrete implementation of the install package command like this:

IoC.Resolve<IPackageCommand>(parsedArgs.First().Key).Execute(packageTree, parsedArgs);

It should be noted that we are resolving the dependecy by the string key AND the type. The string we are using to retrieve the implementation is the first command line switch.
In this case the -install part of the horn -install:horn command.

Each IPackageCommand implementation will be inserted into the container at application start up:

innerContainer.Register(
    Component.For<IPackageCommand>()
                .Named("install")
                .ImplementedBy<PackageBuilder>()
                .LifeStyle.Transient
    );

Once the install command is resolved from the IOC container the following method will be executed.

public void Execute(IPackageTree packageTree, IDictionary<string, IList<string>> switches)
{
   string packageName = GetPackageName(switches);
   IBuildMetaData buildMetaData = GetBuildMetaDataFor(packageTree, packageName);
   IPackageTree componentTree = GetComponentTreeFrom(packageTree, packageName);
   ExecuteSourceControlGet(buildMetaData, componentTree);
   BuildComponentTree(nextMetaData, nextTree);
}

The first step is to retrieve the package name from the right hand side of the install switch.

Once we know which component to retrieve we can get the exact package tree node from the over all package tree.  Typically we have wrapped the package tree implementation in an IPackageTree interface to promote both testability and to decouple the implementation:

public interface IPackageTree : IComposite<IPackageTree>
{
   string Name { get; }
   bool IsRoot { get; }
   Dictionary<string, string> BuildFiles { get; set; }
   IPackageTree Retrieve(string packageName);
   IBuildMetaData GetBuildMetaData(string packageName);
   DirectoryInfo CurrentDirectory { get; }
   DirectoryInfo WorkingDirectory { get; }
   bool IsBuildNode { get; }   
   DirectoryInfo OutputDirectory { get; }
   List<IPackageTree> BuildNodes();
}

The package tree is based on the composite design pattern which states that a group ofobjects can be treated in the same way as a single instance.Each package will have its own child directory structure node of the overall package treecontaining such elements as a WorkingDirectory where the source will be built and an outputdirectory which is the where the output of the build will be placed in the result of an errorless build.
At the time of writing this is the default behaviour of a successful build.

We plan to add to the dsl for post build instructions. Once we have the location for the package's node in the oveall package tree directory structure we can then locate the build.boo file.

The build.boo file contains the package's metadata and install instructions.

Horn's install instructions are defined in the following DSL instance:

install horn:
   description "This is a description of horn"
   get_from svn("https://scotaltdotnet.googlecode.com/svn/trunk/")
   build_with msbuild, buildfile("source/Horn/horn.sln"), frameworkVersion35

The following method takes care of retrieving the file and parsing it's contents:

private IBuildMetaData GetBuildMetaData(IPackageTree packageTree, string packageName)
{
    var buildFileResolver = new BuildFileResolver().Resolve(packageTree.CurrentDirectory, packageName);
    var reader = IoC.Resolve<IBuildConfigReader>(buildFileResolver.Extension);
    return reader.SetDslFactory(packageTree).GetBuildMetaData(packageTree, packageName);
}

This method will return an IBuildMetaData implementation which is defined in the following
contract:

public interface IBuildMetaData
{
    BuildEngine BuildEngine { get; set; }
    SourceControl SourceControl { get; set; }
}

The IBuildMetaData contract has two members, a BuildEngine and a SourceControl member.

SourceControl is an abstract class which will have a number of subclasses to implement the various SourceControl types.  We only have one implementation so far which is the SVNSourceControl subclass which quite obviously is for subversion retrieval.

The SVNSourceControl class uses the excellent SharpSVN open source project to handle the downloading of source code from subversion.

We chose a different route for the BuildEngine's implementation. We favoured composition
over inheritance.

With this in mind, an IBuildTool concrete implementation is passed into
the BuildEngine's constructor:

public BuildEngine(IBuildTool buildTool, string buildFile, FrameworkVersion version)
{
    BuildTool = buildTool;
    BuildFile = buildFile;
    Version = version;
}

The IBuildTool is an abstraction to allow realizations such as MSBuild, Nant
and Rake to be used.

The IBuildTool Contract contains only one method:

public interface IBuildTool
{
    void Build(string pathToBuildFile, IPackageTree packageTree, FrameworkVersion version);
}

Once we have retrieved the source code from the particular SourceControl subclass,
we can then build the source with a concrete IBuildTool implementation.
In the case of horn, we are using an MSBuild subclass as specified in the DSL:
build_with msbuild, buildfile("source/Horn/horn.sln"), frameworkVersion35

The above line specifies which builder we are using, the location the of the build file,
the build tool that will be used and the .NET framework version to build against.

We have plans to create build tool concrete implementations for NAnt, Rake
and just about any other build tool realizatrion that we need.

The IBuildTool implementation for MSBuild is defined as follows:

public void Build(string pathToBuildFile, IPackageTree packageTree, FrameworkVersion version)
{
    var pathToMsBuild = FrameworkLocator.Instance[version].MSBuild.AssemblyPath;
    var args = string.Format("\"{0}\" /p:OutputPath=\"{1}\" /p:TargetFrameworkVersion={2} /p:NoWarn=1591 /consoleloggerparameters:Summary", pathToBuildFile, packageTree.OutputDirectory, GetCmdLineFrameworkVersion(version));
    var psi = new ProcessStartInfo(pathToMsBuild, args)
        {
        UseShellExecute = false,
        RedirectStandardOutput = true,
        WorkingDirectory = packageTree.WorkingDirectory.FullName
        };
    var msBuild = Process.Start(psi);
    while (true)
    {
        var line = msBuild.StandardOutput.ReadLine();
        if (line == null)
            break;
            log.Info(line);
    }
    msBuild.WaitForExit();
}

The previous snippet illustrated the method that will build the source code that we havedownloaded from subversion.

The above method outputs the binary files of the build into the output directory as specified in horn's package tree node.At this time of writing this is where we stand.  The next steps are to do some rigorous refactoring followed by adding a nant IBuildTool implementation.Once we are happy with iteration one and we have a good process for building horn, we can then touch on the main point of the product.

That being dependency management. Which is where it gets very interesting.

Castle Windsor
Rhino.Mocks
XUnit
Rhino.DSL
log4Net

If any of this is of interest to you then please join the Horn user group for updates or check out the source here.

No comments:

Post a Comment