循序渐进——NAnt构建实例
前言
NAnt,一款大名鼎鼎的.NET开源构建工具,功能强大,易于定制。
悲催的是开源的工具往往文档匮乏,广大程序猿们有时发现了看起来很酷的工具,可迟迟无法上手,时间就这么被残酷地浪费掉了。
在园子里搜索了一下,讲“持续集成”或者“每日构建”的不少,合我心意的不多,要么只能入门,要么起点太高。
正好这两天不忙,学习了一下NAnt的使用方法,下面就由我来通过一个实例,演示利用NAnt搭建一个自动化构建环境。
通过本文的构建,最终实现的效果为:
首先从SVN下载最新代码;利用NAnt编译代码;利用NUnit进行单元测试;生成单元测试结果报表以及代码覆盖率报表。
希望通过这篇文章,让打算使用NAnt进行自动化构建的同袍尽快上手。
本文中利用到的工具
NAnt(v0.92)
NUnit(v2.6.2)
OpenCover(v4.0.804)
ReportGenerator(v1.8.1.0)
TortoiseSVN(v1.7.10)
一、利用TortoiseSVN检出项目源码
1、获得工具
TortoiseSVN的项目地址如下:
http://sourceforge.net/projects/tortoisesvn/
2、安装工具
运行安装包,一路下一步即可。
3、检出源码
连接到你自己的代码服务器,检出源码。鉴于TortoiseSVN的易用性相当不错,我就不再罗嗦介绍具体的源码检出方法了,毕竟这并不是本文的重点。
本例中,假设代码服务器上面我们要构建的工程的地址为:http://192.168.1.1/myproject
假设源码检出到本地路径:D:\source\myproject
二、利用NAnt编译C#工程
1、获得工具
NAnt的项目地址如下:
NAnt:http://sourceforge.net/projects/nant/
NAntContrib:http://sourceforge.net/projects/nantcontrib/
NAnt不用多说。NAntContrib是NAnt的扩展,在本例中,需要利用它来生成单元测试报表和SVN控制。
2、安装工具
将NAnt的bin文件夹包含的文件拷贝出来,本例中放置在D:\Tools\NAnt
之后,将NAntContrib的bin文件夹包含的文件也拷贝到D:\Tools\NAnt
在任意位置,建立一个文件nant.bat,文件内容如下:
1 @echo off 2 "D:\Tools\NAnt\NAnt.exe" %*
然后,将nant.bat文件剪切到C:\WINDOWS目录下
运行cmd.exe,在命令行窗口中敲入命令“nant -help”,如果看到NAnt的帮助信息,则说明安装成功。
3、编译源码
首先,在刚刚检出的源码根目录(D:\source\myproject)下建立一个名字为myproject.build的xml文件。
文件内容如下:
1 <?xml version="1.0" encoding="utf-8" ?> 2 <project name="myproject" default="build" basedir="."> 3 <property name="nant.settings.currentframework" value="net-3.5"/> 4 <!-- 源码路径 --> 5 <property name="dir.source" value="D:\source\myproject" /> 6 <property name="dir.source.myexe" value="${dir.source}\myexe" /> 7 <property name="dir.source.mylib" value="${dir.source}\mylib" /> 8 <property name="file.ico.myexe" value="${dir.source.exe}\myexe.ico" /> 9 <!-- 编译结果 --> 10 <property name="dir.release" value="D:\Release" /> 11 <property name="dir.bin" value="${dir.release}\bin" /> 12 <property name="file.exe.myexe" value="${dir.bin}\myexe.exe" /> 13 <property name="file.lib.mylib" value="${dir.bin}\mylib.dll" /> 14 <target name="build" 15 depends="compile"> 16 </target> 17 <target name="compile" 18 depends="mylib,myexe"> 19 </target> 20 <target name="mylib"> 21 <csc target="library" 22 output="${file.lib.mylib}" 23 debug="Full" 24 optimize="true" 25 define="TRACE" 26 platform="AnyCPU" 27 warninglevel="4" 28 rebuild="true" 29 filealign="512"> 30 <sources> 31 <include name="${dir.source.mylib}\**\*.cs" /> 32 </sources> 33 </csc> 34 </target> 35 <target name="myexe" 36 depends="mylib"> 37 <csc target="winexe" 38 output="${file.exe.myexe}" 39 debug="Full" 40 optimize="true" 41 define="TRACE" 42 platform="AnyCPU" 43 warninglevel="4" 44 rebuild="true" 45 filealign="512" 46 win32icon="${file.ico.myexe}"> 47 <sources> 48 <include name="${dir.source.myexe}\**\*.cs" /> 49 </sources> 50 <resources> 51 <include name="${dir.source.myexe}\**\*.resx" /> 52 </resources> 53 <references> 54 <include name="${file.lib.mylib}" /> 55 </references> 56 </csc> 57 </target> 58 </project>
这就是NAnt的构建配置文件了,下面对其中的内容说明一下:
文件主要由两种元素构成:property和target
property用来设置全局变量,以name属性作为唯一标识,使用的时候用${变量名}来引用。
除了自定义的property,NAnt自己也内建了一些全局变量,例如本例中出现的“nant.settings.currentframework”,用来指定当前工程使用的.NET Framework版本。
target是要执行的动作,同样适用name属性作为唯一标识,depends属性用来表示依存关系,例如
1 <target name="myexe" 2 depends="mylib">
上面的配置表示“myexe”这个target执行之前,要先保证mylib被执行了。
target内部可以包含很多Task标签,表示这个target要执行的任务,具体有哪些标签的可以参照NAnt的帮助文档。
最常用的就是csc标签,用来编译C#源码。
大部分csc的属性很好理解,这里强调几个需要特别注意的:
target:这个可不是外层的target标签哦,而是表示要生成什么类型的结果,本例中出现了library(类库)、winexe(窗口程序),还可以设置为exe(控制台程序)。
debug:设置成None的话,就只生成output指定的文件;如果设置成Full,则还会生成pdb文件,这个文件在我们下面进行代码覆盖率计算时需要,因此我们设置成Full。
csc的子标签常用的有三种,本例中都出现了,分别是sources(源码)、resources(资源文件)、references(引用),本文提供的实例应该很好理解,不做说明啦。
特别说明一下路径中使用到的通配符**和*,他们都表示任意文字,区别是**只能用于代表目录,并且可以代表任意级层次的目录,*可以代表目录与文件,但只能代表单级层次的内容,例如:
test1/test2/test3.cs和test1/test2/test3/test4.cs都可以被test1/**/*.cs匹配,而test1/*/*.cs只能匹配到test1/test2/test3.cs
OK,build文件做好了,现在再做一个build.bat文件,内容为:
1 cls 2 nant -buildfile:myproject.build -logfile:build.log
事实上,这两个参数都可选,只打一个命令“nant”也是可以的。
-buildfile参数用来指定build文件,如果不指定的话,会自动搜索当前目录下扩展名为.build的文件,如果存在多个.build文件,则只执行第一个。
-logfile参数用来输出构建过程中的日志,直观的说,就是我们在命令行窗口中看到的文字,都会被输出到指定的日志文件中。
三、利用NAnt自动更新代码
在我们文章的开始,我们是使用TortoiseSVN客户端来检出代码的,但我们想自动化,所以这个动作,也可以交给NAnt来完成。
1、修改.build文件
在.build文件中追加一个target,如下
1 <target name="update"> 2 <svn command="update" 3 destination="${dir.source}" 4 uri="http://192.168.1.1/myproject" 5 verbose="true" 6 quiet="false" 7 /> 8 </target>
然后,再把update动作追加到动作序列里:
1 <target name="build" 2 depends="update,compile">
齐活儿~
四、利用NAnt进行单元测试并生成报表
1、获取工具
NUnit:http://sourceforge.net/projects/nunit/
OpenCover:https://opencover.codeplex.com/
ReportGenerator:https://reportgenerator.codeplex.com/
2、安装工具
NUnit直接执行安装文件,一路下一步。
将OpenCover的解压缩出来,本例中放置在D:\Tools\OpenCover
将ReportGenerator的bin文件夹包含的文件拷贝出来,本例中放置在D:\Tools\ReportGenerator
3、修改.build文件
在.build文件中追加target,如下
1 <target name="mylib.test" 2 depends="mylib"> 3 <csc target="library" 4 output="${file.lib.mylib.test}" 5 debug="None" 6 optimize="true" 7 define="TRACE" 8 platform="AnyCPU" 9 warninglevel="4" 10 rebuild="true" 11 filealign="512"> 12 <sources> 13 <include name="${dir.source.mylib.test}\**\*.cs" /> 14 </sources> 15 <references> 16 <include name="${file.lib.mylib}" /> 17 <include name="${file.lib.nunit.framework}" /> 18 </references> 19 </csc> 20 <copy todir="${dir.bin}" flatten="true"> 21 <fileset> 22 <include name="${file.lib.nunit.framework}" /> 23 </fileset> 24 </copy> 25 </target> 26 <target name="myexe.test" 27 depends="myexe"> 28 <csc target="library" 29 output="${file.lib.myexe.test}" 30 debug="None" 31 optimize="true" 32 define="TRACE" 33 platform="AnyCPU" 34 warninglevel="4" 35 rebuild="true" 36 filealign="512"> 37 <sources> 38 <include name="${dir.source.myexe.test}\**\*.cs" /> 39 </sources> 40 <references> 41 <include name="${file.exe.myexe}" /> 42 <include name="${file.lib.mylib}" /> 43 <include name="${file.lib.nunit.framework}" /> 44 </references> 45 </csc> 46 <copy todir="${dir.bin}" flatten="true"> 47 <fileset> 48 <include name="${file.lib.nunit.framework}" /> 49 </fileset> 50 </copy> 51 </target> 52 <target name="test" 53 depends="mylib.test,myexe.test"> 54 <exec program="OpenCover.Console.exe" basedir="${dir.exe.opencover}"> 55 <arg value="-register:user" /> 56 <arg value="-target:${file.exe.nunit}" /> 57 <arg value="-targetargs:${file.lib.myexe.test} ${file.lib.mylib.test} /result:${file.xml.test.result} /framework:net-3.5 /noshadow" /> 58 <arg value="-output:${file.xml.test.coverage}" /> 59 </exec> 60 <nunit2report format="NoFrames" todir="${dir.report}\NUnit" verbose="true"> 61 <fileset> 62 <include name="${file.xml.test.result}" /> 63 </fileset> 64 </nunit2report> 65 <mkdir dir="${dir.report}" /> 66 <exec program="ReportGenerator.exe" basedir="${dir.exe.repotegenerator}"> 67 <arg value="-reports:${file.xml.test.coverage}" /> 68 <arg value="-targetdir:${dir.report}\OpenCover" /> 69 </exec> 70 </target>
根据之前介绍的内容,这些配置比较好理解了,下面还是挑需要注意的地方讲解一下。
csc的debug属性设置成了None,这是因为测试工程生成的dll不需要进行覆盖率计算,因此不必生成pdb文件。
出现了copy标签,顾名思义,用来拷贝文件。
需要注意flatten属性,这个属性设置成true的意思是,拷贝的文件,不考虑原文件的目录结构,而是直接把原文件拷贝到目标文件夹下。如果设置成false,会把要拷贝的原文件的目录结构一起带过来的呦~
exec标签,用来执行一个外部程序。本例中用来调用OpenCover和ReportGenerator。
需要注意的地方:
1)NUnit是通过OpenCover来调用的,使用的是OpenCover的-target和-targetargs参数。
其中,-targetargs用来提供NUnit的执行参数,这里有点绕,希望注意。
2)NUnit可以同时对多个dll执行测试,多个dll之间用空格隔开。
3)nunit2report标签用来根据单元测试结果xml文件生成单元测试报表。
format属性用来设定报表的形式,NoFrames表示将单元测试结果使用一个html文件来展示;而Frames会把各个单元测试项结果分别生成一个html。本例中是采用了生成单一文件的形式。
4)OpenCover生成的代码覆盖率计算结果文件是一个xml,需要交给ReportGenerator来生成报表
5)ReportGenerator也可以同时处理多个xml文件,利用-reports参数,多个xml文件之间用分号隔开,例如:-reports:xml1;xml2
其他属性嘛,一目了然啊,不罗嗦啦。
五、完成NAnt构建配置
经过上述的配置,基本的自动化流程已经设置好啦。再根据需要进行一些细节处的调整。最终的.build文件如下:
1 <?xml version="1.0" encoding="utf-8" ?> 2 <project name="myproject" default="build" basedir="."> 3 <property name="nant.settings.currentframework" value="net-3.5"/> 4 <!-- 需要利用到的工具 --> 5 <property name="dir.exe.opencover" value="D:\Tools\OpenCover" /> 6 <property name="dir.exe.repotegenerator" value="D:\Tools\ReportGenerator" /> 7 <property name="file.lib.nunit.framework" value="C:\Program Files\NUnit 2.6.2\bin\framework\nunit.framework.dll" /> 8 <property name="file.exe.nunit" value="C:\Program Files\NUnit 2.6.2\bin\nunit-console-x86.exe" /> 9 <!-- 源码路径 --> 10 <property name="dir.source" value="D:\source\myproject" /> 11 <property name="dir.source.myexe" value="${dir.source}\myexe" /> 12 <property name="dir.source.myexe.test" value="${dir.source}\myexe.test" /> 13 <property name="dir.source.mylib" value="${dir.source}\mylib" /> 14 <property name="dir.source.mylib.test" value="${dir.source}\mylib.test" /> 15 <property name="file.ico.myexe" value="${dir.source.exe}\myexe.ico" /> 16 <!-- 编译结果 --> 17 <property name="dir.release" value="D:\Release" /> 18 <property name="dir.bin" value="${dir.release}\bin" /> 19 <property name="file.exe.myexe" value="${dir.bin}\myexe.exe" /> 20 <property name="file.lib.myexe.test" value="${dir.bin}\myexe.test.dll" /> 21 <property name="file.lib.mylib" value="${dir.bin}\mylib.dll" /> 22 <property name="file.lib.mylib.test" value="${dir.bin}\mylib.test.dll" /> 23 <property name="file.pdb.myexe" value="${dir.bin}\myexe.pdb" /> 24 <property name="file.pdb.mylib" value="${dir.bin}\mylib.pdb" /> 25 <!-- 单元测试 --> 26 <property name="dir.report" value="${dir.release}\report" /> 27 <property name="dir.result" value="${dir.release}\result" /> 28 <property name="file.xml.test.result" value="${dir.result}\myproject-results.xml" /> 29 <property name="file.xml.test.coverage" value="${dir.result}\myproject-coverage.xml" /> 30 <target name="build" 31 depends="update,compile,test,clean"> 32 </target> 33 <target name="update"> 34 <svn command="update" 35 destination="${dir.source}" 36 uri="http://192.168.1.1/myproject" 37 verbose="true" 38 quiet="false" 39 /> 40 </target> 41 <target name="compile" 42 depends="mylib,mylib.test,myexe,myexe.test"> 43 </target> 44 <target name="mylib"> 45 <csc target="library" 46 output="${file.lib.mylib}" 47 debug="Full" 48 optimize="true" 49 define="TRACE" 50 platform="AnyCPU" 51 warninglevel="4" 52 rebuild="true" 53 filealign="512"> 54 <sources> 55 <include name="${dir.source.mylib}\**\*.cs" /> 56 </sources> 57 </csc> 58 </target> 59 <target name="mylib.test" 60 depends="mylib"> 61 <csc target="library" 62 output="${file.lib.mylib.test}" 63 debug="None" 64 optimize="true" 65 define="TRACE" 66 platform="AnyCPU" 67 warninglevel="4" 68 rebuild="true" 69 filealign="512"> 70 <sources> 71 <include name="${dir.source.mylib.test}\**\*.cs" /> 72 </sources> 73 <references> 74 <include name="${file.lib.mylib}" /> 75 <include name="${file.lib.nunit.framework}" /> 76 </references> 77 </csc> 78 <copy todir="${dir.bin}" flatten="true"> 79 <fileset> 80 <include name="${file.lib.nunit.framework}" /> 81 </fileset> 82 </copy> 83 </target> 84 <target name="myexe" 85 depends="mylib"> 86 <csc target="winexe" 87 output="${file.exe.myexe}" 88 debug="Full" 89 optimize="true" 90 define="TRACE" 91 platform="AnyCPU" 92 warninglevel="4" 93 rebuild="true" 94 filealign="512" 95 win32icon="${file.ico.myexe}"> 96 <sources> 97 <include name="${dir.source.myexe}\**\*.cs" /> 98 </sources> 99 <resources> 100 <include name="${dir.source.myexe}\**\*.resx" /> 101 </resources> 102 <references> 103 <include name="${file.lib.mylib}" /> 104 </references> 105 </csc> 106 </target> 107 <target name="myexe.test" 108 depends="myexe"> 109 <csc target="library" 110 output="${file.lib.myexe.test}" 111 debug="None" 112 optimize="true" 113 define="TRACE" 114 platform="AnyCPU" 115 warninglevel="4" 116 rebuild="true" 117 filealign="512"> 118 <sources> 119 <include name="${dir.source.myexe.test}\**\*.cs" /> 120 </sources> 121 <references> 122 <include name="${file.exe.myexe}" /> 123 <include name="${file.lib.mylib}" /> 124 <include name="${file.lib.nunit.framework}" /> 125 </references> 126 </csc> 127 <copy todir="${dir.bin}" flatten="true"> 128 <fileset> 129 <include name="${file.lib.nunit.framework}" /> 130 </fileset> 131 </copy> 132 </target> 133 <target name="test" 134 depends="mylib.test,myexe.test"> 135 <exec program="OpenCover.Console.exe" basedir="${dir.exe.opencover}"> 136 <arg value="-register:user" /> 137 <arg value="-target:${file.exe.nunit}" /> 138 <arg value="-targetargs:${file.lib.myexe.test} ${file.lib.mylib.test} /result:${file.xml.test.result} /framework:net-3.5 /noshadow" /> 139 <arg value="-output:${file.xml.test.coverage}" /> 140 </exec> 141 <nunit2report format="NoFrames" todir="${dir.report}\NUnit" verbose="true"> 142 <fileset> 143 <include name="${file.xml.test.result}" /> 144 </fileset> 145 </nunit2report> 146 <mkdir dir="${dir.report}" /> 147 <exec program="ReportGenerator.exe" basedir="${dir.exe.repotegenerator}"> 148 <arg value="-reports:${file.xml.test.coverage}" /> 149 <arg value="-targetdir:${dir.report}\OpenCover" /> 150 </exec> 151 </target> 152 <target name="clean"> 153 <delete dir="${dir.result}" /> 154 <delete> 155 <fileset> 156 <include name="${file.lib.myexe.test}" /> 157 <include name="${file.lib.mylib.test}" /> 158 <include name="${file.pdb.myexe}" /> 159 <include name="${file.pdb.mylib}" /> 160 <include name="${dir.release}\bin\nunit.framework.dll" /> 161 </fileset> 162 </delete> 163 </target> 164 </project>
后记
本来觉得没什么内容,还特意选择了比较简单的场景用来演示,结果写了一下午啊。好吧,我承认我的效率比较低,哈哈。
遗憾之处是还没有集成StyleCop或者FxCop,等我学会了集成它们,再更新这篇文章。
总之,希望此文对需要的朋友有帮助。
文章如有疏漏之处,望读者不吝赐教,板砖粪蛋尽管招呼。