Showing posts with label NAnt. Show all posts
Showing posts with label NAnt. Show all posts

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);
        }
    }
}

Thursday, September 10, 2009

Executing PartCover from NAnt

I wanted to replace NCover with PartCover. PartCover is a new code coverage tool, and its still a freeware.

With NCover I had to run NUnit twice, first to get the unit test results, and then another run to get the code coverage. (At least with the last freeware version of NCover, 1.5.8. I have not tried the commercial versions)

With PartCover its possible to run the tests AND get the coverage at the same time.

Another advantage is that I can use newer versions of NUnit. The NCover 1.5.8 does not work with NUnit from version 2.5.

It seemed rather easy, but I had problems with quotes in the NAnt build file. The PartCover always terminated with an exception, whatever I tried; quotes, variables, expressions:

Invalid option '--target=C:\Program Files\NUnit 2.4.5\bin\nunit-console.exe'

One work-around is to use a NUnit or PartCover configuration file, but I wanted the NAnt build file to be independent.

The solution was to use HTML character entity references, i.e. a double quote (“) can be written as &quot;.

Example of NAnt target that executes PartCover which in turn produces both a coverage report and a unit test report in a specified folder:

  <target name="unitTest">

    <!-- Get all unit test assemblies -->
    <foreach item="File" property="filename">
      <in>
        <items basedir=".">
          <include name="${Build.Output}\bin\${MyProject}.Test.dll"></include>
          <include name="${Build.Output}\bin\${MyProject}.*.Test.dll"></include>
        </items>
      </in>
      <do>

        <echo message="Unittesting ${filename}"/>

        <exec program="${PartCoverHome}\Partcover.exe" failonerror="true">
          <arg line="--target &quot;${NUnitExePath}&quot;" />
          <arg line="--target-work-dir ${Build.Output}\bin"/>
          <arg line="--target-args &quot;${filename} /xml=${Build.Reports}\${path::get-file-name-without-extension(filename)}-UnitTest.xml&quot;" />
          <arg line="--include [${MyProject}.*]*" />
          <arg line="--exclude [${MyProject}.*Test*]*" />
          <arg line="--output ${Build.Reports}\${path::get-file-name-without-extension(filename)}-Coverage.xml" />
        </exec>
      </do>
    </foreach>
  </target>