ExamplesUsing NAnt can be intimidating. That is why I would like to include several examples of building different types of .NET projects. I will try to show off as many of the built-in NAnt tasks as possible. My goal is to give many practical build scripts that can be used in building real life applications. Simple ExeListing 4.10 demonstrates the simplest of NAnt build projects to create an exe file. This simple build is an example included in the NAnt distribution: Listing 4.10. Simple Build<project name="Simple" default="build"> <property name="debug" value="true"/> <target name="clean" description="remove all generated files"> <delete file="Simple.exe" failonerror="false"/> <delete file="Simple.pdb" failonerror="false"/> </target> <target name="build" description="compiles the source code"> <csc target="exe" output="Simple.exe" debug="${debug}"> <sources> <includes name="Simple.cs"/> </sources> </csc> </target> </project> Starting from the top, the project name is very fitting: Simple. The default target that will be executed is "build." The next line is an example of a property that will be used later in the project. The next target, "clean," will never be executed unless specified from the command line. The build target executes the csc task using the debug property set previously and outputs a file called simple.exe, which is an exe file. Notice the use of filesets for specifying the source files. However, there are a few issues that I have with this simplistic project. First, I would change the "includes" line of the source's fileset to use patternsets. Changing the line to <includes name="*.cs"/> would allow you to add files to the directory and project without affecting the build. This is very useful when adding a component, especially because each Windows Form in an executable typically has its own .cs file. Usually, a file only contains a single class and any related functionality. I would also add a few more properties to make the project much more reusable (Listing 4.11). Listing 4.11. Simple EXE Build with Properties<project name="Simple" default="build"> <sysinfo verbose='true'/> <tstamp/> <property name="project.name" value="simple"/> <property name="target.type" value="exe"/> <property name="output.type" value="exe"/> <property name="define" value="DEBUG;TRACE" /> <property name="debug" value="true" /> <property name="build.dir" value="${nant.project.basedir}\Release" /> <if propertytrue="debug"> <property name="build.dir" value="${nant.project.basedir}\Debug" /> </if> <property name="output" value="${build.dir}\${project.name}.${output.type}" /> <property name="doc" value="${build.dir}\${project.name}.xml" /> <target name="clean" description="remove all generated files"> <delete file="${project.name}.${output.type }" failonerror="false"/> <delete file="${project.name}.pdb" failonerror="false"/> </target> <target name="build" description="compiles the source code"> <mkdir dir="${build.dir}" /> <csc target="${target.type}" output="${output}" debug="${debug}" define="${define}" ![]() <sources> <includes name="*.cs"/> </sources> </csc> </target> </project> While this build may look more complicated, it really is not once the concept of properties is understood. By using properties in this manner, this build file can be used as a template for many other types of builds. Simple DLLListing 4.12 shows just how simple it is to change a build template to compile a different type of project. Listing 4.12. Simple DLL Build with Properties<project name="Simple" default="build"> <sysinfo verbose='true'/> <tstamp/> <property name="project.name" value="simple"/> <property name="target.type" value="library"/> <property name="output.type" value="dll"/> <property name="define" value="DEBUG;TRACE" /> <property name="debug" value="true" /> <property name="build.dir" value="${nant.project.basedir}\Release" /> <if propertytrue="debug"> <property name="build.dir" value="${nant.project.basedir}\Debug" /> </if> <property name="output" value="${build.dir}\${project.name}.${output.type}" /> <property name="doc" value="${build.dir}\${project.name}.xml" /> <target name="clean" description="remove all generated files"> <delete file="${project.name}.${output.type }" failonerror="false"/> <delete file="${project.name}.pdb" failonerror="false"/> </target> <target name="build" description="compiles the source code"> <mkdir dir="${build.dir}" /> <csc target="${target.type}" output="${output}" debug="${debug}" define="${define}" ![]() <sources> <includes name="*.cs"/> </sources> </csc> </target> </project> This builds all the cs files in the current directory into a dll. Notice the only difference between Listing 4.11 and 4.12 is that target.type and output.type are changed to output a dll. ASP.NET and Web ServicesBuilding an ASP.NET or Web Service project is essentially just the same as building a simple dll. ASP.NET assumes the dll to be in a bin subdirectory. The biggest difference is that with Web-based products, NAnt can also be useful in deploying the redistributables. While NAnt certainly could be used in installing traditional thick clients, NAnt's ease of use really is displayed best in the Web development environment.Chapter 6, "Unit Testing." Listing 4.13. WebService Build<project name="Webservice" default="build"> <sysinfo verbose='true'/> <tstamp/> <property name="project.name" value=" Webservice1"/> <property name="target.type" value="library"/> <property name="output.type" value="dll"/> <property name="define" value="DEBUG;TRACE" /> <property name="debug" value="true" /> <property name="build.dir" value="${nant.project.basedir}\bin" /> <property name="output" value="${build.dir}\${project.name}.${output.type}" /> <property name="doc" value="${build.dir}\${project.name}.xml" /> <property name="localpath" value="C:\inetpub\wwwroot\${project.name}" /> <target name="clean" description="remove all generated files"> <delete file="${project.name}..${output.type }" failonerror="false"/> <delete file="${project.name}..pdb" failonerror="false"/> </target> <target name="deploy" description="Create Virtual Directory and copy redistributables"> <mkdir dir="${localpath}" /> <mkdir dir="${localpath}\bin" /> <mkiisdir dirpath="${localpath}" vdirname="${project.name}" authntlm="true"/> <copy todir="${localpath}"> <fileset> <includes name="**\*.aspx"/> <includes name="**\*.asax"/> <includes name="**\*.asmx"/> <includes name="**\*.ashx"/> <includes name="**\*.config"/> </fileset> </copy> <copy todir="${localpath}\bin" file="${output}" /> </target> <target name="build" description="compiles the source code"> <mkdir dir="${build.dir}" /> <csc target="${target.type}" output="${output}" debug="${debug}" define="${define}" ![]() <sources> <includes name="*.cs"/> </sources> </csc> </target> </project> After building this project, the Webservice can be tested in Internet Explorer using the auto-generated test page. Windows ServiceWindows Services are the same as a simple exe, but to install the service into Windows, Service Control Manager (SCM) requires the use of the NAntContrib <installutil> task in Listing 4.14. Listing 4.14. Building and Installing a Windows Service with NAnt<project name="Simple" default="build"> <sysinfo verbose='true'/> <tstamp/> <property name="project.name" value="simpleservice"/> <property name="target.type" value="exe"/> <property name="output.type" value="exe"/> <property name="define" value="DEBUG;TRACE" /> <property name="debug" value="true" /> <property name="build.dir" value="${nant.project.basedir}\Release" /> <if propertytrue="debug"> <property name="build.dir" value="${nant.project.basedir}\Debug" /> </if> <property name="output" value="${build.dir}\${project.name}.${output.type}" /> <property name="doc" value="${build.dir}\${project.name}.xml" /> <target name="clean" description="remove all generated files"> <delete file="${project.name}.${output.type }" failonerror="false"/> <delete file="${project.name}.pdb" failonerror="false"/> </target> <target name="build" description="compiles the source code"> <mkdir dir="${build.dir}" /> <csc target="${target.type}" output="${output}" debug="${debug}" define="${define}" ![]() <sources> <includes name="*.cs"/> </sources> </csc> </target> <target name="install" description="installs the exe as a windows service"> <servicecontrol machinename="." servicename="${project.name}" command="stop" failonerror="false" /> <installutil assembly="${output}" /> <servicecontrol machinename="." servicename="${project.name}" command="start" failonerror="false" /> </target> <target name="uninstall" description="uninstalls the exe as a windows service "> <servicecontrol machinename="." servicename="${project.name}" command="stop" failonerror="false" /> <installutil assembly="${output}" uninstall="true" /> </target> </project> Windows FormA Windows form project is again very similar to a simple exe project, except for the use of license files and resource files. Resource files can also be used in ASP.NET projects, as seen in Listing 4.15. Listing 4.15. Windows Form Build<project name="Simple" default="build"> <sysinfo verbose='true'/> <tstamp/> <property name="project.name" value="simplewinform"/> <property name="target.type" value="winexe"/> <property name="output.type" value="exe"/> <property name="define" value="DEBUG;TRACE" /> <property name="debug" value="true" /> <property name="build.dir" value="${nant.project.basedir}\Release" /> <if propertytrue="debug"> <property name="build.dir" value="${nant.project.basedir}\Debug" /> </if> <property name="output" value="${build.dir}\${project.name}.${output.type}" /> <property name="doc" value="${build.dir}\${project.name}.xml" />> <target name="clean" description="remove all generated files"> <delete file="${project.name}.${output.type }" failonerror="false"/> <delete file="${project.name}.pdb" failonerror="false"/> </target> <target name="build" description="compiles the source code"> <mkdir dir="${build.dir}" /> <license input="licenses.licx" output="${output}.licenses" licensetarget="} ![]() <assemblies> <includes name="${build.dir}\bin\${licenses.dotnetbar}"/> <includes name="${build.dir}\bin\${licenses.ultrawingrid}"/> </assemblies> </license> <resgen input="${project.name}.resx" output="${build.dir}\ ${project.name}.resources" /> <comregister unregister="false"> <fileset> <includes name="treegrid.OCX"/> </fileset> </comregister> <aximp ocx="treegrid.ocx" keyfile="strongname.key"/> <csc target="${target.type}" output="${output}" debug="${debug}" define="${define}" ![]() <sources> <includes name="*.cs"/> </sources> <references> <includes asis="true" name="System.dll"/> <includes asis="true" name="System.Data.dll"/> <includes asis="true" name="System.Drawing.dll"/> <includes asis="true" name="System.Windows.Forms.dll"/> <includes asis="true" name="System.XML.dll"/> <includes name="treegrid.dll"/> </references> <arg value="/resource:${build.dir}\${project.name}.resources" /> </csc> </target> </project> Listing 4.15 shows the use of a few references, resource files, and an ActiveX OCX control. The use of a license file is simply for illustration in case a third-party assembly requires it; a license file is not very useful here in my opinion. Unsafe, Interop, and OtherThere are so many tasks and so much functionality to apply that I devote an entire chapter to a case study of NAnt and other tools that we will cover in the next few chapters. Handling Large ProductsRemember, architecting a build is much more than just understanding NAnt. Architecting a successful build of a large product containing many projects can be a full-time job. The build, being code, should also have documentation and well thought-out structure. Figure 4-2 is a UML Use Case Diagram showing a simple build. Each of the UML Use Cases actually is a build target. Figure 4-2. UML Use Case Diagram of the Build.![]() Listing 4.16. Using Foreach to Get All Files in a Directory<foreach item="Folder" property="foldername"> <in> <items> <includes name="*"/> <excludes name="CVS"/> </items> </in> <do> <nant buildfile="${foldername}/default.build" target="${project.config} ${target}"/> </do> </foreach> This goes through each subdirectory and executes the <nant> task on the default.build file. This also passes in the configuration and target to the subproject. So in this design, there is a Master project that has the "get" task and that knows where everything is in SCM and where it is going on the file system. Then all other tasks are just dummy tasks that are passed on down to the subprojects. Therefore, the subprojects have knowledge about how to build, clean, and lint themselves, but not how to get themselves because you would have already had to get the project out of SCM before running the NAnt build anyway. This fulfills the general build principle that the Master build can conveniently get everything out and structure it for you, but because all other knowledge is self-contained in the subproject, you could also just run the get task of a specific subproject to wherever working directory you want to and build it using its NAnt project. The problem with this approach is that it does not handle inter-project dependencies. This design will happily recurse the subdirectories and build each project, but what if one project depends upon another? Unless the directory structure was such that the projects were built in the correct order, this build will fail.Another similar approach is the one used by NAnt itself. Yes, NAnt is built using NAnt! This approach is to use the foreach task again but to put the list of projects into an XML file. This file could contain the dependency information and be transformed to the appropriate text file since <foreach> currently only works on text files. Listing 4.18 shows the Master build's XML list of projects to build, complete with dependency information. Listing 4.17 is the Helper Application to transform Listing 4.18 to a projects.txt file, and Listing 4.19 is the actual NAnt Master build project. Listing 4.17. Helper Application to Order the Master Build in the Correct Orderusing System; using System.Collections; using System.Diagnostics; using System.IO; using System.Xml; using System.Xml.XPath; using System.Xml.Xsl; namespace NAnt { class DependancyChecker { private static ArrayList al; private static XmlDocument xdoc; [STAThread] static void Main(string[] args) { try { if(args.Length>0) { al = new ArrayList(); StreamReader sr = new StreamReader(args[0]); xdoc = new XmlDocument(); xdoc.Load(sr); XmlNodeList projectNodes = xdoc.SelectNodes("/projects/project"); foreach(XmlNode projectNode in projectNodes) { ProcessProjectNode(projectNode); } sr.Close(); StreamWriter sw = new StreamWriter("projects.txt"); for(int i=0; i<al.Count; i++) { sw.WriteLine(al[i]); } sw.Close(); } else { Console.WriteLine("A path to the dependency file is required!"); } } catch(Exception ex) { Console.WriteLine(ex.ToString()); } } private static void ProcessProjectNode(XmlNode projectNode) { if(projectNode.SelectNodes("dependencies").Count > 0 ) { XmlNodeList dependencyNodes = projectNode.SelectNodes("dependencies/dependency"); foreach(XmlNode dependencyNode in dependencyNodes) { ProcessDependencyNode(dependencyNode); } if(!al.Contains(projectNode ["name"].InnerText)) al.Add(projectNode ["name"].InnerText); } else { if(!al.Contains(projectNode["name"].InnerText)) al.Add(projectNode ["name"].InnerText); } } private static void ProcessDependencyNode(XmlNode dependencyNode) { XmlNodeList projectNodes = xdoc.SelectNodes("/projects/project [name='" + ![]() if(projectNodes.Count > 0 ) { XmlNodeList dependencyNodes = projectNodes [0].SelectNodes("dependencies"); if(dependencyNodes.Count > 0 ) { ProcessProjectNode(projectNodes [0]); } else { if(!al.Contains(dependencyNode.InnerText)) al.Add(dependencyNode.InnerText); } } } } } Listing 4.18. The XML List of Projects to Build<?xml version="1.0"?> <projects> <project> <name>middletier\ws</name> <dependencies> <dependency>middletier\datalib</dependency> </dependencies> </project> <!-- <project> <name>presentation\testapp</name> <dependencies> <dependency>middletier\ws</dependency> </dependencies> </project> --> <project> <name>presentation\widgetapp</name> <dependencies> <dependency>middletier\ws</dependency> </dependencies> </project> <project> <name>middletier\datalib</name> </project> </projects> Listing 4.19. Master Build File<project name="Master Build" basedir="." default="build"> <sysinfo verbose='true'/> <property name="debug" value="true" /> <property name="define" value="DEBUG;TRACE" /> <property name="build.dir" value="C:\dev" /> <property name="refassemblies" value="${build.dir}\refassemblies " /> <property name="ssdir" value="\\server\SourceSafe\vss\srcsafe.ini" /> <property name="ssuser" value="User1" /> <property name="sspassword" value=" /> <target name='get'> <delete dir="${build.dir}" failonerror="false"/> <mkdir dir="${build.dir}" /> <vssget localpath="${build.dir}\Genghis" recursive="true" dbpath="${ssdir}" user="${ssuser}" password="${sspassword}" replace="true" path="$/3rd_party/Genghis" writable="true"/> <vssget localpath="${build.dir}" recursive="true" dbpath="${ssdir}" user="${ssuser}" password="${sspassword}" replace="true" path="$/software" writable="true"/> </target> <exec program="NantHelper.exe" commandline="projects.xml" output="projects.txt" ![]() <!-- After applying transform pass the target to the subprojects --> <target name='clean' depends='transform'> <foreach item='Line' property='filename' in='projects.txt'> <nant buildfile='${build.dir}\${filename}.build' target='build' /> </foreach> </target> </project> The output of the NAnt exec task (that runs the Helper Application) is a text file that is properly ordered for dependencies. The advantage of using XML to store the list of projects is that it is transformable and you can easily temporarily comment out projects you do not want to build. Target dependencies are handled in the subprojects. Chapter 9 will elaborate on this design in detail. |