Saturday, November 5, 2005

NAnt - The solution task and setup/merge module projects in VS.NET 2003

At my current employer we use CruiseControl.net. Before we had CruiseControl.net in place we used NAnt build files. I'm sure that if you done much more than a "hello world" solution using NAnt you would soon find that there are some project types that do not work well with NAnt (at least 0.85 RC2).

I've given up on getting solutions that contain C++ in them to compile correctly with NAnt. It is just much easier to use an exec task. All that being said, I really dislike seeing warning messages of any kind in the build results:

Warnings: (22)

Only C#, VB.NET and C++ projects are supported. Skipping project 'FirstModule\FirstModule\Module.vdproj'.

Only C#, VB.NET and C++ projects are supported. Skipping project 'AnotherModule\AnotherModule.vdproj'.

Only C#, VB.NET and C++ projects are supported. Skipping project 'AnnoyMeModule\AnnoyMeModule.vdproj'.


The above is a sample with the module names changed. I knew from hacking solution files in the past that it would be possible to simply find the references to the MergeModule projects and remove them and then load the solution.

What I needed to do was remove the lines below:

Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "FirstModule", "FirstModule\FirstModule.vdproj", "{675E01F7-2494-4F95-A271-B2F6F6FD02D4}"
ProjectSection(ProjectDependencies) = postProject
EndProjectSection
EndProject


I thought about three possible solutions to removing these lines:

  1. Write an EXE Console App that used Regex's to find the 'offending' lines and remove them from the output (an "old school" Unix filter!)

  2. Take a similar approach but implement a new NAnt task. I've had to do that in the past to override broken CVS tasks, so this wasn't a bad option.

  3. Write a script task and embed the C# code directly into the build file. Not as elegant, but extremely portable and you don't need to load a DLL to get it working.

Here is the script task that I wrote. It’s not pretty, and certainly could be cleaned up a little, but it does the job:

<!-- Utility Scripts -->

<script language="C#" prefix="filter">

    <code>

    <![CDATA[

    [Function("remove-vdproj")]

    public static string RemoveVdprojFilter(string path)

    {

        string beginPattern = @"^Project\(""\{54435603-DBB4-11D2-8724-00A0C9A8B90C\}""\) = "".+"", "".+\.vdproj"", ""\{[A-F0-9\\-]+\}""$";

        string endPattern = @"^EndProject$";

 

        using (StreamReader reader = File.OpenText(path))

        using (StringWriter writer = new StringWriter())

        {

            bool inside = false;

            string line;

            while ((line = reader.ReadLine()) != null)

            {

                if (inside)

                {

                    Match match = Regex.Match(line, endPattern, RegexOptions.IgnoreCase);

                    inside = match.Success == false;

                }

                else

                {

                    Match match = Regex.Match(line, beginPattern, RegexOptions.IgnoreCase);

                    inside = match.Success;

                    if (inside == false)

                    {

                        writer.WriteLine(line);

                    }

                }

            }

 

            return writer.ToString();

        }

    }

    ]]>

    </code>

</script>



The workhorse of this script is the beginPattern. It reads in the solution file line by line until it finds the project definition that matches a Setup/MergeModule project. It then changes the state to 'inside' and reads until it finds the EndProject line. This will continue until it processes the entire file. The filtered solution file is returned as a string.

I decided to use an 'echo' task to write the filtered solution file back to the disk. It is then ready for consumption in a solution task.

NOTE: You might run into problems if you call this NAnt task from another NAnt task. The script code assumes the working directory of the original Nant project, not the currently executing task's project's directory. To work around this, make sure to surround your relative paths to your solution file with a path::get-full-path() function.

See example below:

<target name="ds.build" description="Builds DirectoryServices">

    <echo file="DirectoryServices/DirectoryServices-NoMerge.sln" message="${filter::remove-vdproj(path::get-full-path('DirectoryServices/DirectoryServices.sln'))}" />

    <solution configuration="${configuration}" solutionfile="DirectoryServices/DirectoryServices-NoMerge.sln" />

    <delete file="DirectoryServices/DirectoryServices-NoMerge.sln" />

</target>



You can easily expand on this concept to alter solution and project files prior to compiling them in your favorite default.build!

Scott pointed out to me that I should cross-link to his article, "Building MSI files from NAnt and Updating the VDProj's version information and other sins on Tuesday". Thanks Scott!

-- John

2 comments:

  1. It'd be nice to link back.

    ReplyDelete
  2. I'm not up on the blog linking etiquette. Yes, indeed, I will mod the post and include the link.

    ReplyDelete