分享

Managing .NET Development with NAnt

 amine 2010-12-10

January 29, 2004

Why NAnt

Microsoft has released their most complete and powerful IDE yet in Visual Studio .NET. From the vast library of project templates, to the incredibly easy to use WinForms designer, to solid tools for visually integrating databases, VS.NET provides an excellent environment for the individual developer.

There is a lot more to creating an application than just writing and compiling code, however. Visual Studio .NET, for all its powerful features, cannot be all things to all developers. Specifically, Visual Studio .NET provides little support for:

  • Customized deployment scenarios
  • Distributed team development
  • Centralized integration and compilation
  • Automated testing and reporting
  • Source control tools, other than Visual Source Safe
  • Automated pre- and post-processing of project files

For example, let's say you are working on a project with 9 other programmers. Five of you are in Los Angeles, and five in Boston. The SourceSafe server is in Chicago, as is the staging server. When you want to work on a single code file, it's as easy as checking it out, editing it, and checking it in. But what if you need to do a sanity check of your file as part of the build? Well, then you have to go through the SourceSafe database and check out all the different project trees, and build them (in the correct order) just to make sure your component compiles ok. Then, if you want to run unit tests, you have to fire up NUnit and walk through all the test libraries manually.

Your job as the build engineer is even more difficult. Somehow, you have to find a way to check out the full version of the project every night at midnight, build everything (in the correct order), run the unit tests, email the compile and test results to every member of the team, and if everything else went ok, zip the binaries up and ftp them to the distribution server.

Visual Studio provides some of the support for the first scenario (developer on a team) and almost none for the second (build engineer). If you have to execute these tasks manually, you open yourself to a slew of errors (missed files in the checkout, forgotten test libraries, mistyped commands). These repetitive tasks cry out for automation. Historically, this kind of automation was handled with tools like make and nmake. These tools rely on a proprietary syntax for writing a build file which executes a series of commandline tools to build your project.

NAnt is an open source alternative to make. NAnt is easy to learn, stable, efficient, and best of all, free. It stands above other build tools because it uses XML for its file structure, which is more easily readable by humans and manipulated by code. Additionally, there is a robust mechanism for testing the success of each individual build step and halting the process upon failure.

For an individual developer, NAnt is an important tool for managing a project; for a distributed team, NAnt is essential.


An Introduction to NAnt

NAnt is deceptively simple. An NAnt build file is comprised of four kinds of items: tasks, targets, properties and projects. Everything that can be done with NAnt is done with one of these four types. All are represented in a standard XML syntax.


Tasks

Tasks are the NAnt version of commands. Most NAnt tasks come with NAnt itself, though NAnt is extensible through third-party command libraries or writing your own. Tasks can represent simple shell commands (copy, delete, touch), complex system commands (zip, unzip, mail, cvs), .NET-specific commands (csc, vbc, tlbimp), and miscellaneous commands (nunit, regex).

NAnt also has tasks that make it operate somewhat like a declarative language: tasks like if, foreach, and script allow you to insert flow-control and looping structures, as well as arbitrary executable script statements into the build file.

A task is a specifically named XML element, with standardized attributes and sub-elements that make up the options for the represented command. For example, to make a backup copy of an important file, you would use the copy task:

	<copy file="importantFile.cs" tofile="importantFile.bak"/>

or, to send an email, you would use the mail task:

<mail  from="buildfile@mycompany.com"
tolist=devteam@mycompany.com
subject="Nightly build results"
mailhost="smtp.mycompany.com">

<attachments>
<includes name="*report.txt"/>
</attachments>
</mail>

The full list of standard tasks is available at http://nant./help/tasks/index.html. The NAntContrib project (http://nantcontrib.) provides a wide variety of additional tasks, from reporting project statistics (codestats) to interacting with IIS (mkiisdir, deliisdir) to executing SQL statements (SQL).


Targets

Targets are named collections of tasks. Think of tasks as individual commands, and targets as named methods. Executing a target causes each task contained in that target to execute, in the order they are found.

<target name="prepare" description="creates folders: bin, src, doc">
<mkdir dir="bin"/>
<mkdir dir="src"/>
<mkdir dir="doc"/>
</target>

Targets can depend on other targets to execute. Using the depends attribute, you can specify one or more other targets that must execute prior to the current target. The depends attribute takes a comma-separated list of target names as its value; the targets listed will be executed in order from left to right.

It is an immutable rule of NAnt that a target is executed only once during a given build event. Even if multiple executing targets depend on one shared target, the shared target will execute only once (when the first dependent target is executed). Subsequent dependent targets will see that the requested target has executed already.

<target name="clean">
<del dir="src"/>
<del dir="bin"/>
<del dir="doc"/>
</target>

<target name="prepare" depends="clean">
<mkdir dir="src"/>
<mkdir dir="bin"/>
<mkdir dir="doc"/>
</target>

Whenever the prepare target executes, NAnt will verify that clean has already run. If it has, prepare is allowed to continue; if it has not, clean is run first, then prepare. Keep in mind that regardless of how many targets depend on the same target, each target is executed only once.


Properties

Properties are named data storage for a given build file. Though you can think of them as variables, that isn't technically correct since property values can be set exactly once; thereafter, a property value is immutable. Properties can be supplied a default value in the build file, or can be read from an external file or have their values supplied by arguments on the command that launched the build.

NAnt supplies a series of default properties available to any project, from nant-specific information (like nant.version, nant.filename) to .NET-specific information (like nant.settings.defaulframework and nant.settings.defaultframework.sdkdirectory). You can create your own properties by supplying a name and optional default value.

<property name="host.name" value="relevancellc.com"/>
<property name="ftp.target.site" value="ftp.relevancellc.com"/>

Projects

A project is a collection of properties, targets and tasks. A project can reside in a single XML (build) file, or can span multiple files. It defines the collection of targets and properties needed to manage your development project.

	<project name="myProject">
<property name="project.author" value="Justin Gehtland"/>

<target name="clean">
<del dir="src"/>
<del dir="bin"/>
</target>

<target name="prepare" depends="clean">
<mkdir dir="src"/>
<mkdir dir="bin"/>
</target>

...
<!-- etc. -->
...
</project>

Projects have three attributes that you can modify: name, default, and basedir. Name is an identifying string (see above example). Default is the name of the target to execute if none is otherwise supplied. Basedir is the location to use as the root of any relative file system operations; if you do not supply a value for basedir, NAnt will use the folder containing the executing build file.


Running Projects

To run a project, invoke nant.exe and pass in the name of the build file using the /f flag:

C:>nant /f:default.build

If you omit the build file name, NAnt uses the following algorithm to determine what to execute:

  1. Search the current folder for a build file.
  2. If no build file is found, return an error.
  3. If only one build file is found, execute it.
  4. If more than one build file is found:
    • If one is called default.build, execute it.
    • If none are called default.build, return an error.

In addition to build file, you can pass in a variety of other information to nant, including the target framework version, a log file to output to, whether or not to display debug information in the output, etc. Perhaps most important, you can pass in values for properties in the build file using the -D flag. For instance, to run myProject above and override the default value of project.author, you could issue the following command:

C:>nant /f:myProject.build -D:project.author="Albert Einstein"

In addition, you can specify a target to execute. If you do not specify one, the value of the default attribute of the project will be used. To specify a different target, just append the target names you desire in a list as the final arguments on the nant command:

C:>nant -D:project.author="Albert Einstein" clean prepare

NAnt and the Visual Studio Project File

A very common use for a build file is to compile a project; .NET developers commonly use Visual Studio .NET to compile their project, but can also use the command-line compilers (csc.exe, vbc.exe, etc.). Visual Studio .NET manages the complexities of compilation, such as external references and output types. Using the command-line compilers requires managing those complexities yourself.


Inside the Visual Studio Project File

The Visual Studio .NET project file (i.e. *.csproj) is an XML file describing the files and settings that make up a given project. Inside the root <VisualStudioProject> element is an element describing the type of project file; <CSHARP> or <VisualBasic>, for instance.

Beneath this element are two sections: Build and Files. Files is a simple list of all associated files and how to treat them.

<File RelPath="Form1.cs" SubType="Form" BuildAction="Compile"/>
<File RelPath="App.ico" BuildAction="Content"/>

Build contains two further subsections: Settings and References. Settings captures all the information you can set in the Project -> Properties dialog, including global settings plus anything specific to the Debug or Release builds. References lists all the external assemblies that this project depends upon.

<Reference Name="System" AssemblyName="System" 
HintPath="..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.dll"/>
<Reference Name="nunit.framework" AssemblyName="nunit.framework"
HintPath="\Program Files\NUnit V2.1\\bin\nunit.framework.dll"/>

Together, Visual Studio .NET uses all of this data to create the appropriate compilation command to create your project.


Avoid Executing Devenv.exe

Many beginning NAnt users make the mistake of piggy-backing on Visual Studio .NET (devenv.exe) to compile their projects. Since the Visual Studio .NET project file already contains all the necessary data about the project, it is easy to invoke devenv.exe using the exec task and point it at the project file.

There are two significant drawbacks to this style of compilation:

  1. The overhead cost of running Visual Studio .NET is quite large. Any process that invokes it is going to drain a lot of system resources. An automated script (like a NAnt build file) that may be invoked often, can quickly run into resource problems.
  2. Not all machines that need to build the project will have Visual Studio .NET installed. Any central build-and-test server, in fact, should NOT have it installed.

Avoid using devenv.exe to compile your projects. Instead, use NAnt's built-in csc and vbc tasks. Doing so means that build machines need only have the .NET Framework installed (a given for .NET projects) and use the much more efficient command-line compilers to do their work.

The problem with this approach is the complexity of the csc and vbc tasks once your project expands beyond a simple HelloWorld. Not only do the tasks have 21 possible attributes you can, and in some cases must, set, but it has sub-elements for:

  • Search paths for referenced assemblies
  • The referenced assemblies themselves
  • Embedded resources
  • All source files
  • And any other arbitrary arguments to the compile command

As your project changes over time, you have to keep your build file in synch with the current state. This is tedious, and doing it manually is error-prone.


Using SLiNgshoT

Luckily, there are other options for keeping the build file in synch with the project file. The NAntContrib project comes with a tool called SLiNgshoT which will parse the project file and create a skeleton build file for you. The resulting file contains fully formatted csc (or vbc) tasks as appropriate, in addition to a variety of other default targets which you can modify to your liking.

SLiNgshot does a good job of quickly creating skeletal build files for you; however, it is difficult to use SLiNgshoT with a project that changes often, since it regenerates the entire build file. Any edits you made to the last version are lost; you have to copy them back in yourself, which is again tedious and error prone. In addition, regardless of how correct the resulting build file is, you are stuck with the structure it provides (unless you undertake significant editing).


Using a Custom Transform

A more powerful alternative is to use a custom XSLT transform to create the compilation task for you automatically. It can be used to either create a stand-alone build file, or to modify the existing build-file by simply replacing the existing compile statements.

Gordon Weakliem has an excellent entry on this topic on his blog (http:://radio.weblogs.com/0106046/stories/2002/08/10/supportingVsnetAndNant.html).


Using the Solution task (NAnt 0.8.3 or later)

Starting in release 0.8.3-rc1, the NAnt core tasks include <solution>. This task processes a given solution file, compiling the project(s) contained within. The <solution> task recognizes inter-project dependencies and compiles them in the correct order. The <solution> task does not invoke devenv.exe, but rather parses the solution file and invokes the command-line compiler.

The <solution> task allows you to provide specific build information on a per-configuration basis (different arguments for debug vs. release). You can limit which projects in the solution to build by passing in a list of projects in a <projects> element, or a list of projects to exclude in <excludeprojects>.

If the projects in your solution depend on the output of another project (from a different solution) you can make them depend on its output via the <referenceprojects> element. This allows you to specify any number of project files that need to be fully compiled before your solution file can itself be compiled. NAnt will check to see if the referenced projects' binary output is up-to-date; if so, the target solution will reference the existing assemblies. Otherwise, the referenced projects are compiled first.

Additionally, you can control the search paths for referenced assemblies using the <assemblyfolders> element. Use this task when your target solution relies on third-party assemblies that exist only in binary format. This element allows you to provide the compiler hints about where referenced assemblies may be found on the file system.

The <solution> task is the easiest solution for integrating with Visual Studio .NET project files, but it requires that your project have an actual solution file (.sln) instead of just a project file (.csproj, .vbproj). If you require more control over the build parameters than is provided by this task, you are better off writing a custom transform as in the previous section.


NAnt and the Distributed Team

NAnt is very useful for single-developer projects. You can easily control everything from building deliverables to creating documentation to deployment from a single build file. However, as the development team grows and the complexity of the application increases, NAnt becomes even more useful. Integrated development environments like Visual Studio .NET simply can't manage all aspects of such a project. NAnt, as a tool and an extensible framework, offers the perfect platform for controlling the complexity.

Enterprise-level applications are often a collection of interrelated subprojects. They typically contain some kind of client application, a database, and any number of layers of code in between the two. Individual developers may work on a single subproject, or might be required to work in any or all of them depending on project need. NAnt provides a mechanism for ensuring that all developers work with the right files and perform the right pre- and post-build actions to keep in synch with the rest of the team.


Standardized Build Files

In order to effectively use NAnt across multiple related projects, your developer team must define a standard set of targets that make up a build file, suitable to your team's goals and the type of application you are developing. The canonical set of primary targets for a build file (see Hatcher and Loughrin's Java Development with Ant) are:

  • Init: prepares your environment for a successful build (creates folders, copies initial files, etc.)
  • Build: actually compile the application, creating any distributable application files
  • Docs: create any documentation
  • Test: run the unit tests
  • Dist: distribute the final artifacts (binary application, source code, documentation)
  • Clean: remove any build artifacts, leaving just the source
  • All: run all of these targets, in this order.

Typically, all of these targets are part of a single dependency chain except Clean. So, Build depends on Init. Docs and Test depend on Build, and Dist depends on Docs and Test. Clean, however, should be able to be run on its own so that, at any point, a developer can eliminate any cruft that might have built up in the development directories. The All target is then used to run all of the targets (assuming, of course, there are no failures).

Projects might contain any number of specialized targets that enable these primary targets. Perhaps one project requires a series of specialized XSLT transforms to create the documentation (using the <style> task) while another one uses the <ndoc> task to accomplish the same thing. Each project might require a different set of supporting tasks to create the initial context for the unit tests. These specialized targets provide utility support for the primary targets common to all the projects that make up your application.


Working with Parent and Child Projects

Each subproject in your application will have its own build file. Thus, any project can be built in isolation from the rest of the project, making the development team and process as modular as possible. When the entire application needs to be built, tested and/or deployed, though, you don't want to have a completely different build file for managing each individual subproject. Instead, you create a parent build file which relies on the specific build files for each subproject to do the work.

The <nant> task allows you to specify a build file and, optionally, a specific target in that build file to execute. Perhaps the most vital property of the <nant> task is the inheritall property. This specifies whether a child build launched from the parent will inherit all the property values defined in the parent (the property defaults to false). If you choose not to inherit the values, or the child build file has properties not defined in the parent, you can use the <properties> subelement to define any other property values for the child build.

The best way to organize the parent build file is to create a new target for each subproject's build file. Then, create a dependency chain between the targets that mimics the dependencies between the subprojects. For instance, say your project consists of a library of business logic accessible by both a web service and a standard web site. Both the web service project and the web site project have a dependency on the business logic project. Therefore, your parent build file would look like:

<project name="myApplication" default="default">
<target name="businessLogic">
<nant buildfile="businesslogic/default.build"
inheritall="true"/>
</target>
<target name="webService" depends="businessLogic">
<nant buildfile="webservice/default.build"
inheritall="true"/>
</target>
<target name="webSite" depends="businessLogic">
<nant buildfile="website/default.build"
inheritall="true"/>
</target>
<target name="default" depends="businessLogic, webService,
webSite"/>
</project>

Now, you can build the entire application by executing the default target of the parent build. Conversely, you can use the parent to build any single child project, respecting inter-project dependencies (since both webService and webSite depend on businessLogic, the businessLogic project will always be built and its binaries available for the other two).

To make it easier to clean up after the build, you will also want a global clean target in the parent build.

<target name="clean">
<nant buildfile="businessLogic/default.build" target="clean"/>
<nant buildfile="webService/default.build" target="clean"/>
<nant buildfile="webSite/default.build" target="clean"/>
</target>

Call this once to clean out all artifacts from all parts of the build tree.


NAnt and Other Tools


When the Core Tasks Aren't Enough

NAnt comes with a wide assortment of built-in tasks. Combined with the tasks available from the NAntContrib project, and it might seem at first glance that there is not much NAnt can't do. There will come a time when you need to make use of some process, application or resource that isn't represented in the core tasks. It might be a new tool that you just purchased, or a script written by your team to perform a specific task.

There are two standard tasks that can be used to do this: <exec> and <script>. <exec> wraps a shell command and its arguments in order to launch arbitrary applications. <script> wraps a block of script, which is executed by NAnt. The problem with both is that they are not conducive to reuse across projects; to accomplish the same task in two different projects, you would be forced to resort to copy-and-paste reuse. If, later, you discover a problem with the logic or data, you have to change it everywhere manually.

Fortunately, NAnt itself is extensible. The more reusable strategy is to create your own NAnt task. Creating one is simplicity itself. Simply create a new class which derives from NAnt.Core.Task (or one of the more specific base classes in NAnt.Core.Tasks, like ExternalProgramBase, which wraps calling an external program). Decorate your class with the TaskName attribute from the NAnt.Core.Attributes namespace; this determines what the XML name for your task will be.

Next, you must define the properties of your task that will be available in the build file. Create public properties on your class and decorate them with the TaskAttribute attribute (also found in NAnt.Core.Attributes). This value will hook up XML attributes of the given name to this property of the class.

Finally, override the ExecuteTask method of the base class to do the work. To report results back to the NAnt output stream, write them using System.Console.Write (or WriteLine).

For example, you might want to create a task for encrypting a file. Such a task would, among other things, need to allow the build file author to specify the key and a salt value for the encryption. The task in a build would look like:

<crypto key="SOME_KEY_VALUE" salt="SOME_SALT_VALUE" file="plaintext.txt"/>

The implementing class looks like this:

using NAnt.Core;

namespace Relevance.NAnt
{
[NAnt.Core.Attributes.TaskName("crypto")]
public class CryptoTask : NAnt.Core.Task
{
private string _file;
private string _key;
private string _salt;

[NAnt.Core.Attributes.TaskAttribute("file")]
public string File
{
get { return _file; }
set { _file = value; }
}

[NAnt.Core.Attributes.TaskAttribute("key")]
public string Key
{
get { return _key; }
set { _key = value; }
}

[NAnt.Core.Attributes.TaskAttribute("salt")]
public string Salt
{
get { return _salt; }
set { _salt = value; }
}

protected override void ExecuteTask()
{
//Encrypt the file
//...

//Report results
System.Console.WriteLine("File: {0} encrypted.",
_file);
}
}
}

To install the tasks, your assembly's name must be in the form NAnt.xxx.Tasks.dll, where xxx is some descriptive name for your task(s). Drop this file into the same directory where nant.exe lives (or a folder called "tasks" beneath it) and your new task is available for builds.


Authoring NAnt Build Files

At the end of the day, a NAnt build file is an XML document. Visual Studio .NET currently has no support for authoring these build files other than the XML editor (which you will have to configure to treat .build files as XML). Some developers use XML editors like XMLSpy (www.) or TurboXML (www.), while others stick to the basics with Notepad or emacs.

For developers who want more assistance writing and maintaining their build files, there is a (currently) free tool for visually authoring them called Nantpad (www.). Nantpad provides a standard tree-and-list view for editing the build files. The true value of Nantpad is that you can feed it any valid NAnt schema, and Nantpad they automatically provides the tasks available in that version of NAnt. Better still, the context menus then display available tasks for instertion, or sub-elements of existing tasks. If you are like this author, and believe that Intellisense® is one of the best things to ever happen to programmers, then Nantpad is definitely the way to author your build files.


Continuous Integration with CruiseControl.NET

Many development teams believe that regular integration builds are important. They create NAnt build scripts which live on the master build server and are scheduled to run every night, or they manually kick them off on Friday afternoon as everyone heads home for the weekend. While these regularly scheduled builds are quite useful, more useful still is the idea of continuous integration.

Continuous integration is the Holy Grail of an agile development process. It is the idea that all code entered into the source control repository is integrated with existing code, built and tested as soon as possible after it is checked in. The process should be automatic, and produce a running log of builds and the results. Martin Fowler has written the canonical introduction to CI at http://www./articles/continuousintegration.html. It is not enough to have builds run every weekend or overnight; builds must happen as soon as check-ins occur.

CruiseControl.NET (http://www.) is a server-based application that monitors your source code repository. As code is checked in, CruiseControl retrieves the whole project tree, executes a NAnt-based build, runs tests and files a report on a browser-accessible log. CruiseControl can also be configured to notify team members of build events via email. The team monitors the results of the builds to keep track of the health of the application; broken builds are identified immediately, and the offending code can be addressed right away.


Moving From nmake

If you are working on a project with one or more nmake build files already, the easiest way to start moving to NAnt is to make a new .build file and call out to the existing nmake files as appropriate. For example, if you have a project consisting of five independent components, three of which already have nmake builds associated with them, you could make a NAnt build file that natively compiles the other two using <csc> or <vbc>, but uses <exec> to launch the three existing nmake builds. You can then layer in tasks for unit testing, deployment, documentation, etc. as needed. Later, as time permits, you can go back and translate the nmake builds into native NAnt format.


Looking Ahead to MSBuild

Let it never be said that Microsoft has difficulty recognizing a good thing. NAnt (and its progenitor, Ant) have proven to be industry-changing technologies. The functionality offered by NAnt is a superset of the build control features available in Visual Studio, and the XML format of the build scripts is infinitely more manageable and flexible than the Project -> Properties available inside the IDE.

In the upcoming release of Visual Studio (codenamed "Whidbey") a new tool called MSBuild will be available. XML-based build files, which are collections of properties, items, tasks and groups, will be executed by a console application called MSBuild.exe. Tasks are implemented as classes that extend MSBuild.Task in the MSBuild assembly. Visual Studio project files (.csproj, .vbproj) will abandon their proprietary XML vocabulary and instead become MSBuild scripts, which can then be extended by the developer to include other tasks besides compilation, just like a NAnt script. Although not available in current beta releases of Visual Studio .NET, we can expect visual designers for MSBuild files to appear by the time the final release is available.

Authors

Justin Gehtland is a founding member of Relevance, LLC, a consultant group dedicated to elevating the practice of software development. He is the co-author of Windows Forms Programming in Visual Basic .NET (Addison Wesley, 2003) and Effective Visual Basic (Addison Wesley, 2001). Justin is an industry speaker, and instructor with DevelopMentor in the .NET curriculum.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多