在.NET環境中實現每日構建(Daily Build)--NAnt篇
在.NET環境中實現每日構建--NAnt篇
前言
關于每日構建這個話題,也已經有很多很好的文章討論了。 本文的寫作過程中也參考了這些文章。本文之所以繼續這個題目,是因為在查閱了網上的資源后,發現沒有一個比較通用的過程。所以本文就主要討論了利用 NAnt構建一個通用日編譯的方案。利用這個方案,日編譯的維護者可以不需要對每個要編譯的方案都要做很多維護。只要定義一個屬性文件就可以了。
關鍵詞: Daily Build, NAnt
1. 簡介
1.1. 每日構建的優點:
每日構建(Daily Build)也可稱為持續集成(Continuous Integration),強調完全自動化的、可重復的創建過程,其中包括每天運行多次的自動化測試。每日構建的作用日益顯得重要。它讓開發者可以每天進行系統集成,從而減少了開發過程中的集成問題。
持續集成可以減少集成階段"捉蟲"消耗的時間,從而最終提高生產力。它使得絕大多數bug在引入的同一天就可以被發現。而且,由于一天之中發生變動的部分并不多,所以可以很快找到出錯的位置。
1.2. 每日構建完成的任務
實現自動化每日構建需要做以下幾部分的工作:
l 使創建過程完全自動化,讓任何人都可以只輸入一條命令就完成系統的創建。
l 使測試完全自動化,讓任何人都可以只輸入一條命令就運行一套完整的系統測試。
l 確保所有人都可以得到最新、最好的可執行文件。
2. 每日構建所使用的工具
在.NET環境下建立每日構建可以使用一系列開源工具:
Nant: 完成代碼的自動編譯,自動運行測試工具。http://nant.sourceforge.net/builds/
NantContrib:自動從源碼庫中獲取源代碼。http://nantcontrib.sourceforge.net/nightly/builds/
NUnit2Report:將NUnit測試工具產生的XML報告轉換為HTML報告形式。http://NUnit2Report.sourceforge.net
VSS:Visual Source Safe,微軟源碼管理工具
Draco.NET: 用于自動檢測VSS中源代碼變動情況,調用Nant完成自動編譯
http://sourceforge.net/projects/draconet/
下載所需的工具后,按照如下步驟進行安裝:
在服務器上安裝VSS源碼管理工具
安裝下載的Draco Server 和 Draco Web,修改安裝后的Draco Web目錄下的web.config文件,設置正確的Draco Server安裝路徑
將NAnt、NAntContrib、NUnit2Report壓縮包解壓,將三個Bin目錄中的內容復制到一個公用目錄,比如D:\DailyBuildTools,然后將該路徑加入系統的Path路徑列表中,具體為“控制面板-〉系統屬性-〉環境變量-〉Path”
3. NAnt自動腳本
NAnt腳本實現了每日構建的主體功能,它具體分為下面幾部分
l 定義每日構建所需的一些環境變量,比如從VSS上下載的源碼的保存目錄,發布目錄等
l 清除舊的代碼并從VSS源碼庫中下載最新源代碼
l 編譯源代碼并運行測試代碼集
l 將編譯后的目標代碼拷貝到發布目錄進行發布
為了盡可能少的改動NAnt的腳本文件,簡化日常維護的工作量,我們把一些對所有項目都基本相同的過程抽取出來,如環境變量定義,清除舊代碼獲取新代碼,編譯源代碼,對目標代碼進行發布的過程都可以寫成通用的腳本,而一個具體項目的每日構建腳本則調用通用過程完成
本文采取的目錄體系如下所示:
D:\DailyBuild\
<project1>\Source:存放<project1>源代碼的目錄
<project1>\Build:存放<project1>編譯后的目標代碼的目錄
<project1>\Publish:存放<project1>的WEB發布文件的目錄
<project1>\log:存放<project1>的日志文件
3.1. Nant的基礎知識
l Nant腳本代碼文件的基本結構
<?xml version="1.0" encoding="gb2312"?>
<project name="Projects" default="prebuild">
<target name="prebuild" depends="namecheck,clean " description="…">
……
</target>
<target name="namecheck" >
……
</target>
</project>
說明:encoding="gb2312"使得腳本文件可以支持中文
<project>標簽定義了項目屬性,一個腳本文件只能有一個項目定義
default="prebuild"說明該項目缺省從prebuild任務開始執行
<target>標簽定義了一項任務,任務是Nant腳本具體執行動作的最小單元
depends="namecheck,clean "說明該任務執行前需要namecheck和clean任務先執行
description描述了該任務的一些說明性信息
l 定義變量
<property name="<變量名>" value="$<變量值>"/>
如上所示,定義變量使用<property>標簽,name屬性定義了變量的名稱,value屬性定義變量的值,其中name屬性可以使用字母、數字、點號、下劃線等符號,而value屬性可以使用字符串或是已經定義的變量,Nant內建的函數等,
要使用已經定義的變量,可以用${<變量名>},要使用內建函數,可以使用${<函數名稱>}
如: <property name="solution.basedir" value="${core.basedir}\${solution.name}"/>
使用了已定義變量core.basedir和solution.name來定義變量solution.basedir;
<property name="curdir" value="${directory::get-current-directory()}"/>
使用了NAnt內建函數directory::get-current-directory()來定義curdir變量
定義環境變量的腳本代碼寫在Common。Config文件里
主要有以下幾類信息的定義:
l 每日構建所在的根目錄
<property name="curdir" value="${directory::get-current-directory()}"/>
<property name="core.basedir" value="${curdir}"/>
說明:${directory::get-current-directory()}內建函數獲取當前文件所在路徑信息
l 被編譯的解決方案的目錄結構,和前面提到的目錄體系一致
<property name="solution.basedir" value="${core.basedir}\${solution.name}"/>
<property name="solution.source" value="${solution.basedir}\source"/>
<property name="solution.build" value="${solution.basedir}\build"/>
<property name="solution.log" value="${solution.basedir}\log"/>
說明:以上代碼是定義了要編譯的解決方案的目錄結構信息,其中${solution.name}是由外部傳入的解決方案的名稱,后面的代碼將根據該名稱在日編譯的根目錄下生成和solution.name指定的名稱同名的目錄,并在該目錄下生成source,buld,log等子目錄
l VSS源代碼管理系統的基本信息
<!--vss數據庫登錄信息-->
<property name="vss.username" value="autobuild"/>
<property name="vss.password" value="autobuild"/>
<!--vss數據庫所在的位置-->
<property name="vss.dbpath" value="\\10.136.238.231\vss\srcsafe.ini"/>
<!--vss中工程的根目錄-->
<property name="vss.basepath" value="$/"/>
說明:定義了和VSS源碼管理系統相關的一些信息,其中VSS數據庫所在位置可以是網絡路徑,也可以是本地路徑
l <編譯時的一些參數
<!--編譯版本號-->
<property name="build.number" value="1.0"/>
<!--決定編譯是Debug版本還是Release版本-->
<property name="build.configuration" value="Release"/>
3.3. 建立目錄結構,獲取源代碼
腳本代碼寫在CheckSource.build.xml文件里
l 包含在Common.config文件里定義的公共變量
<include buildfile="common.config"/>
l 檢查是否存在solution.name變量
<target name="namecheck" description="檢查solution.name變量是否設置">
<!--檢查解決方案名稱是否已經定義-->
<ifnot test="${property::exists('solution.name')}">
<fail message="未定義解決方案名稱solution.name"/>
</ifnot>
<!--去掉可能的空格字符-->
<property name="solution.name" value="${string::trim(solution.name)}"/>
<!--檢查solution.name變量是否為空字符-->
<if test="${string::get-length(solution.name)==0}">
<fail message="未定義解決方案的名稱solution.name"/>
</if>
</target>
說明:${property::exists('<變量名>')}是NAnt內建函數,用于測試某變量是否存在
${string::get-length(<字符串變量>)==0}測試字符串的長度是否為0
<ifnot test=<邏輯表達式> … </ifnot>:如果test表達式值為假,執行<ifnot>標簽內的代碼
<if test=<邏輯表達式> … </if>:如果test表達式值為假,執行<if>標簽內的代碼
l 建立解決方案的目錄結構
<target name="clean" depends="namecheck" description="移除舊目錄,建立新目錄">
<!--刪除舊的解決方案代碼所在目錄-->
<delete dir="${solution.basedir}" failonerror="false"/>
<!--重新建立目錄-->
<mkdir dir="${solution.basedir}\" failonerror="false"/>
<mkdir dir="${solution.source}" failonerror="false"/>
<mkdir dir="${solution.build}" failonerror="false"/>
<mkdir dir="${solution.log}" failonerror="false"/>
</target>
說明:delete和mkdir標簽內的failonerror屬性表示即使操作文件夾的過程中出現了錯誤,也忽略錯誤向下執行
l 獲取源代碼:
從VSS上獲取解決方案<solution.name>的源代碼
<target name="getsourcecode">
<!--檢查從VSS上下載解決方案的路徑是否設定-->
<!-- 如果不定義vss.projectpath,則缺省為solution.name -->
<ifnot test="${property::exists('vss.projectpath')}">
<property name="vss.projectpath" value="${solution.name}"/>
</ifnot>
<vssget
user="${vss.username}"
password="${vss.password}"
localpath="${solution.source}"
recursive="true"
replace="true"
dbpath="${vss.dbpath}"
path="${vss.basepath}${vss.projectpath}"
/>
</target>
說明:<vssget>標簽是NAntContrib的語法,用來從VSS源碼管理器上下載源代碼,user和password屬性表示登錄VSS服務器的信息;Localpath屬性是指下載的源代碼存放的路徑;recursive="true"表示遞歸獲取代碼;replace="true"表示如果本地有重復文件,則進行覆蓋;dbpath定義VSS的srcsafe.ini文件的路徑信息,包括srcsafe.ini文件名;path定義了要獲取的源代碼在VSS數據庫中的路徑,一般都是以$/為根目錄。
3.4. 編譯源代碼
l 編譯命令
編譯解決方案的命令為
<solution solutionfile="…" configuration="…" outputdir="…">
<webmap>
<map url="… " path="…"/>
<map url="… " path="…"/>
</webmap>
</solution>
其中solutionfile屬性表明了要編譯的解決方案文件的路徑信息,即以"sln"為擴展名的文件,
configuration屬性表明要編譯的是發行版還是調試版,取值為"Release"或"Debug"
outputdir表明了編譯后的動態鏈接庫或可執行文件存放的目錄
solution中的嵌套標簽<webmap>用于當解決方案含有WEB項目的情況,有幾個WEB項目,就有幾項<map>標簽,map標簽中的url屬性為WEB項目的*.csproj文件的WEB路徑,path則為該*.csproj文件所在磁盤上的物理路徑,例如,解決方案中有WEB項目exam,則map標簽為 <map url="http://localhost/exam/exam.csproj" path="c:\exam\exam.csproj"
l 根據解決方案名稱獲取解決方案文件的路徑信息
<target name="build" description="編譯解決方案">
<!-- 查找解決方案文件名 -->
<foreach item="File" property="filename">
<in>
<items>
<include name="**\${solution.name}.sln"/>
</items>
</in>
<do>
<!--根據文件名設置解決方案的名稱-->
<property name="solution.file" value="${filename}"/>
</do>
</foreach>
說明:<foreach>標簽是NAnt中處理循環的命令,item="File"說明foreach進行循環處理的對象是文件,<include>中的name變量表示要查找的文件信息,"**\"表示查找路徑包括子目錄。Foreach的屬性property="<變量名>"表示查找到的文件路徑信息保存在該變量中,可以在<do>標簽中引用.foreach每查找到一項符合條件的Item,都會執行<do>標簽中的代碼,以上代碼執行的結果就是查找到指定名稱的解決方案文件,供后面編譯代碼使用
l 獲取解決方案中WEB項目的路徑信息
如果解決方案中含有WEB項目,則其編譯命令和不含WEB項目的解決方案編譯有所區別,所以要區別對待。如果解決方案含有多個WEB項目,則可以讓用戶將多個WEB項目的名稱放在一個變量中,如solution.webprojects,以逗號或分號或空格做分隔符。然后將項目名稱分別提取出來,根據Web項目的個數決定solution命令的形式,代碼如下
<!--將solution.webprojects中用",",";"或" "分隔的Web工程名提取出來,
分別設為webproject1,webproject2 -->
<if test="${property::exists('solution.webprojects')}">
<foreach item="String" in="${solution.webprojects}" delim=";, " property="project">
<if test="${property::exists('webproject1')}">
<property name="webproject2" value="${project}"/>
</if>
<ifnot test="${property::exists('webproject1')}">
<property name="webproject1" value="${project}"/>
</ifnot>
</foreach>
</if>
以上代碼中foreach標簽的屬性item="String" in="${solution.webprojects}" delim=";, " property="project"表明循環對象是字符串,對in所代表的字符串
如果設定solution.webprojects="webprj1;webprj2”,則以上代碼執行的結果是定義了兩個變量webproject1 ="webproj1"和webproject2 ="webproj2"
l 查找WEB工程名
根據前面從solution.webprojects中提取出來的webproj1和webproj2變量,查找該WEB工程的文件名
<!-- 查找WEB工程文件名 -->
<if test="${property::exists('webproject1')}">
<echo message="test ${webproject1}" />
<foreach item="File" property="filename">
<in>
<items>
<include name="**\${webproject1}.csproj"/>
</items>
</in>
<do>
<!--根據Web項目的名稱獲取Web項目文件路徑,可以處理兩個Web項目的情況-->
<echo message="WebProject file=${filename}"/>
<property name="webproject1.file" value="${filename}"/>
</do>
</foreach>
</if>
同理可以處理存在第二個WEB工程項目的情況,設置webproject2.file變量
l 編譯解決方案
最后是編譯解決方案,分別根據無WEB項目,有2個WEB項目,有一個WEB項目的三種情況處理
下面僅列出有兩個WEB項目的情況
<!-- 存在2個Web工程 -->
<if test="${property::exists('webproject2')}">
<solution
solutionfile="${solution.file}"
configuration="${build.configuration}"
outputdir="${solution.build}"
>
<webmap>
<map
url="http://localhost/${webproject1}/${webproject1}.csproj"
path="${webproject1.file}"
/>
<map
url="http://localhost/${webproject2}/${webproject2}.csproj"
path="${webproject2.file}"
/>
</webmap>
</solution>
</if>
3.5. 運行測試代碼
l 測試命令
NAnt中關于測試的命令是<NUnit2>標簽
<nunit2>
<formatter type="Xml" usefile="true"
extension=".xml" outputdir="…"
/>
<test assemblyname="…" haltonfailure="false" />
</nunit2>
說明:<formatter>標簽中,type="Xml"表明了根據測試結果生成XML結構化信息,usefile="true"表明使用文件保存測試結果,extension=".xml"表明生成的文件擴展名為xml,outputdir指出了文件將被保存到哪個目錄
Test標簽中的assemblyname表明了被測試的dll程序集的路徑信息,haltonfailure="false"表明即使測試沒有通過仍然繼續執行腳本文件
這樣在測試命令完成后,會在outputdir指出的目錄下生成一個XML形式的報告文件,為了增加測試結果的可讀性,可以使用另一個工具NUnit2Report,將測試結果轉換為直觀的HTML文件。具體命令如下
<nunit2report out="<文件名>" todir="<輸出目錄" >
<fileset>
<includes name="<文件匹配符>" />
</fileset>
</nunit2report>
說明:includes標簽用來搜索符合條件的XML文件,轉換出來的HTML文件保存為out指出的文件名,todir指出了HTML文件將保存的目錄信息
<if test="${property::exists('solution.testprojects')}">
<foreach item="String" in="${solution.testprojects}" delim=";, " property="project">
<property name="testfile" value="${solution.build}\${project}.dll"/>
<nunit2>
<formatter type="Xml" usefile="true"
extension=".xml" outputdir="${solution.build}" />
<test assemblyname="${testfile}" haltonfailure="false" />
</nunit2>
<nunit2report out="${project}.html" todir="${solution.log}" >
<fileset>
<includes name="${solution.build}\*.xml" />
</fileset>
</nunit2report>
</foreach>
</if>
3.6. 進行WEB發布
WEB發布主要針對有WEB工程項目的解決方案,其實現原理為利用NAnt的拷貝命令,將WEB工程下除了源代碼,資源代碼,VSS信息文件外的其他文件和編譯后的程序集拷貝到發布目錄,最后設置WEB虛擬路徑以供WEB訪問的過程。
設置WEB虛擬路徑的命令為
<mkiisdir dirpath="<物理路徑>" vdirname="<虛擬路徑>"/>
說明:設WEB項目發布在C:\Intepub\wwwroot\Exam,訪問該WEB項目用地址http://127.0.0.1/Example/default.aspx,則<物理路徑>為"C:\Intepub\wwwroot\Exam",虛擬路徑為"Example"(此處略去詳細代碼)。
下載示例代碼