Friday, October 8, 2010

Simple NAnt dependency manager for the TeamCity repository

I did not found any suitable tool that can be used to load dependencies when building .NET project in a TeamCity environment (using NAnt scripts).
Until now all dependencies where stored in VCS together with the source, not so sophisticated.

All our TeamCity projects contains one SDK-configuration that is a zipped file with all output assemblies and executables.

The TeamCity offers URL patterns to access build artifacts. For example:

http://teamcity.mycompany.com/guestAuth/repository/download/AGroup::AProduct/1.1.124/SDK/AProduct_SDK-1.1.124.zip

These two facts trigged me to build a custom NAnt task.
It simply downloads SDK-artifacts from the TeamCity repository, and unpacks the assemblies in the current build environment before the build is started.

I added a “pom”-file to all TeamCity configuration projects that defines the dependencies:

<project>
 <dependencyManagement>
   <repositories>
     <repository>http://teamcity.mycompany.com/guestAuth/repository/download/</repository>
   </repositories>
   <dependencies>
     <dependency>
       <groupId>AGroup</groupId>
       <artifactId>AProduct</artifactId>
       <version>1.1.124.0</version>
     </dependency>
     <dependency>
       <groupId>AGroup</groupId>
       <artifactId>BProduct</artifactId>
       <version>5.4.42.0</version>
     </dependency>
     <dependency>
       <groupId>BGroup</groupId>
       <artifactId>CProduct</artifactId>
       <version>2.5.24.0</version>
     </dependency>
   </dependencies>
 </dependencyManagement>
</project>

The NAnt task is executed in the build file:

<target name="loadDependencies">
  <loaddependencies filename="${Build.Base}\dependencies.xml" target="${Build.Base}\Dependencies"/>
</target>
The two parameters defines the name of the dependency file and where I want the assemblies to be copied.

And here is the source if someone is interested:

using System;
using System.Collections.Generic;

namespace MyNAnt.Build.Tasks
{
    using System.IO;
    using System.Net;
    using System.Xml;

    using global::NAnt.Core;
    using global::NAnt.Core.Attributes;
    using ICSharpCode.SharpZipLib.Zip;

    [TaskName("loaddependencies")]
    public class LoadDependenciesTask : Task
    {
        // NAnt parameters
        [TaskAttribute("filename", Required = true)]
        [StringValidator(AllowEmpty = false)]
        public string DependencyFile { get; set; }

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

        // Lokal parameters
        private List<Dependency> Dependencies { get; set; }
        private List<string> Repositories { get; set; }

        /// <summary>
        /// Executes the NAnt task
        /// </summary>
        protected override void ExecuteTask()
        {
            // load all dependency information
            LoadDependencies();

            // download dependencies from TeamCity
            foreach (var dependency in Dependencies)
            {
                DownloadArtifact(Repositories, dependency.Group, dependency.Name, dependency.Version, Target);
            }
        }

        /// <summary>
        /// Reads the dependency list from configuration file
        /// </summary>
        private void LoadDependencies()
        {
            var doc = new XmlDocument();

            Log(Level.Info, "Loading dependencies from '{0}'.", DependencyFile);
            doc.Load(DependencyFile);

            // load all repositories to search for assemblies
            var repList = new List<string>();
            var repElemList = doc.GetElementsByTagName("repository");
            foreach (XmlNode repository in repElemList)
            {
                repList.Add(repository.InnerText);
            }
            Repositories = repList;

            // load all dependency assemblies
            var depList = new List<Dependency>();
            var depElemList = doc.GetElementsByTagName("dependency");
            foreach (XmlNode dependency in depElemList)
            {
                var item = new Dependency();
                if (dependency != null)
                {
                    item.Group = dependency["groupId"].InnerText;
                    item.Name = dependency["artifactId"].InnerText;
                    item.Version = dependency["version"].InnerText;
                }
                depList.Add(item);
            }
            Dependencies = depList;
        }

        /// <summary>
        /// Downloads and unzip artifact from TeamCity repository
        /// </summary>
        /// <param name="repositoryList"></param>
        /// <param name="group"></param>
        /// <param name="name"></param>
        /// <param name="version"></param>
        /// <param name="destination"></param>
        private void DownloadArtifact(List<string> repositoryList, string group, string name, string version, string destination)
        {
            Log(Level.Info, "Destination folder: '{0}'.", destination);

            // create destination folder if it not exist
            Directory.CreateDirectory(destination);

            foreach (var repository in repositoryList)
            {
                // URL-example
                // http://teamcity.mycompany.com/guestAuth/repository/download/AGroup::AProduct/1.1.124/SDK/AProduct_SDK-1.1.124.zip
                var address = repository + group + "::" + name + "/" + version + "/SDK/" + name + "_SDK-" + version + ".zip";

                using (var wc = new WebClient())
                {
                    try
                    {
                        using (var streamRemote = wc.OpenRead(new Uri(address)))
                        {
                            Log(Level.Info, "Found artifact '{0}'.", address);

                            var zis = new ZipInputStream(streamRemote);
                            ZipEntry ze;
                            while ((ze = zis.GetNextEntry()) != null)
                            {
                                if (ze.IsDirectory)
                                {
                                    Directory.CreateDirectory(ze.Name);
                                }
                                else
                                {
                                    var buffer = new byte[2048];
                                    var fileName = Path.GetFileName(ze.Name);

                                    Log(Level.Info, "Unzipping '{0}'.", fileName);

                                    using (
                                        Stream outstream = new FileStream(
                                            destination + "\\" + fileName, FileMode.Create))
                                    {
                                        while (true)
                                        {
                                            var bytes = zis.Read(buffer, 0, 2048);
                                            if (bytes > 0) outstream.Write(buffer, 0, bytes);
                                            else break;
                                        }
                                    }
                                }
                            }
                            return;
                        }
                    }
                    catch (Exception)
                    {
                        // ignore exceptions
                    }
                }
            }

            Log(Level.Error, "ERROR: Artifact '{0}::{1}' with version '{2}' not found!.", group, name, version);
        }
    }
}