[獨孤九劍]持續集成實踐(二)– MSBuild語法入門

本系列文章包含:html

[獨孤九劍]持續集成實踐(一)- 引子git

[獨孤九劍]持續集成實踐(二)– MSBuild語法入門github

[獨孤九劍]持續集成實踐(三)- Jenkins安裝與配置(Jenkins+MSBuild+GitHub)web

 

一、開始                                                                                                                       

在這篇文章中,咱們會從頭開始,一步步完成一個屬於咱們本身的MSBuild腳本。在它完成之後,咱們只須要一個命令就能夠刪除以前的構建產物,構建.NET應用,運行單元測試。後面咱們還會配一個Jenkins Job,讓它從代碼庫中更新代碼,執行MSBuild腳本。最後還會配另外一個Jenkins Job,讓它監聽第一個Job的結果,當第一步成功之後,它會把相關的構建產物複製出來,放到web服務器裏啓動運行。服務器

咱們用一個ASP.NET MVC 3應用作例子,在VS裏面建立ASP.NET MVC 3應用並選擇「application」模版就行。咱們還要用一個單元測試項目來跑測試。代碼能夠在這裏下載。【因爲個人機器環境沒法跑通他給的例子,所以我簡單的建立了另外一個webForm項目用於測試,若是你一樣沒法跑起來HelloCI這個項目,而且懶癌嚴重,請點擊這裏下載個人代碼】app

二、你好,MSBuild                                                                                                          

MSBuild是在.NET 2.0中引入的針對Visual Studio的構建系統。它能夠執行構建腳本,完成各類Task──最主要的是把.NET項目編譯成可執行文件或者DLL。從技術角度來講,製做EXE或者DLL的重要工做是由編譯器(csc,vbc等等)完成的。MSBuild會從內部調用編譯器,並完成其餘必要的工做(例如拷貝引用──CopyLocal,執行構建先後的準備及清理工做等)。框架

這些工做都是MSBuild執行腳本中的Task完成的。MSBuild腳本就是XML文件,根元素是Project,使用MSBuild本身的命名空間。異步

MSBuild文件都要有Target。Target由Task組成,MSBuild運行這些Task,完成一個完整的目標。Target中能夠不包含Task,可是全部的Target都要有名字。編輯器

下面來一塊兒建立一個「Hello World」的MSBuild腳本,先保證配置正確。我建議用VS來寫,由於它能夠提供IntelliSense支持,不過用文本編輯器也無所謂,由於只是寫個XML文件,IntelliSense的用處也不是很大。先建立一個XML文件,命名爲「basics.msbuild」,這個擴展名只是個約定而已,好讓咱們容易認出這是個MSBuild腳本,你倒不用非寫這樣的擴展名。給文件添加一個Project元素做爲根元素,把 http://schemas.microsoft.com/developer/msbuild/2003設置成命名空間,以下所示單元測試

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>

下一步,給Project元素添加一個Target元素,起名叫「EchoGreeting」

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="EchoGreeting" />
</Project>

這就好了。咱們已經有了一個能夠運行的MSBuild腳本。它雖然還啥事都沒幹,但咱們能夠用它來驗證當前環境是否是能夠運行MSBuild腳本。

在運行腳本的時候,咱們要用到.NET框架安裝路徑下的MSBuild可執行文件。打開命令行,執行「MSBuild /nologo /version」命令,看看.NET框架安裝路徑是否是放到了PATH環境變量裏面。若是一切正確,你應該能看到屏幕上打印出MSBuild的當前版本。假若沒有的話,或者把.NET框架安裝路徑放到PATH裏面去,或者直接用Visual Studio Command Prompt,它已經把該配的都配好了。【個人Path裏沒有,因此要配置PATH環境變量,機器是Win7 x64的,VS2013,MSBuild.exe文件的Bin目錄位置在C:\Program Files (x86)\MSBuild\12.0\Bin】

進入存放剛纔那個腳本的目錄後,以文件名看成參數調用MSBuild,就能夠執行腳本了。在個人機器上能夠看到下面的執行結果:

C:\>msbuild basics.msbuild

Microsoft (R) Build Engine Version 4.0.30319.1
[Microsoft .NET Framework, Version 4.0.30319.269]
Copyright (C) Microsoft Corporation 2007. All rights reserved.
Build started 8/2/2012 5:59:45 AM.

Build succeeded.

0 Warning(s)
0 Error(s)

Time Elapsed 00:00:00.03

執行完腳本之後,MSBuild會首先顯示一個啓動界面和版權信息(用 /nologo 開關能夠隱藏掉它們)。接下來會顯示一個啓動時間,而後即是真正的構建過程。由於我們的腳本啥都沒幹,因此構建就直接成功了。總計用時也會顯示在界面上。下面我們來給EchoGreeting Target添加一個Task,讓腳本真的乾點事。【如下內容在練習是必定要注意拼寫錯誤,不要問我爲何。。。】

<Target Name="EchoGreeting">
    <Exec Command="echo Hello from MSBuild" />
</Target>

如今EchoGreeting Target有了一個Exec Task,它會執行Command屬性中定義的任何命令【Command裏的命令應該都是批處理命令】。再運行一次腳本,你應該能看到更多信息了。在大多數時候,MSBuild的輸出信息都很長,你能夠用 /verbosity 開關來只顯示必要信息【使用MSBuild /help可查詢全部命令參數】。不過不管怎樣,MSBuild都會把咱們的文字顯示到屏幕上。下面再添加一個Target。

<Target Name="EchoDate">
    <Exec Command="echo %25date%25" />
</Target>

這個Target會輸出當前日期。它的命令要作的事情就是「echo %25date%25」,可是「%」字符在MSBuild中有特殊含義,因此這個命令須要被轉義。當遇到轉義字符的時候,「%」後面的十進制字符會被轉成對應的ASCII碼。MSBuild只會執行Project元素中的第一個Target。要執行其餘Target的時候,須要把/target開關(可簡寫爲 /t)加上Target名稱傳給MSBuild。你也能夠指定MSBuild執行多個Target,只要用分號分割Target名字就能夠。

C:\>msbuild basics.msbuild /nologo /verbosity:minimal /t:EchoGreeting;EchoDate
Hello from MSBuild
Thu 08/02/2012

三、更實用的構建腳本                                                                                                         

演示就先到這裏。下面來用MSBuild來構建一個真實項目。首先把示例代碼下載下來,或是本身建立一個ASP.NET應用。給它添加一個MSBuild腳本,以solution或project名字給腳本命名,擴展名用「.msbuild」。照先前同樣指定MSBuild命名空間。

開始寫腳本以前,先把腳本要乾的事情列出來:

1. 建立BuildArtifacts目錄

2. 構建solution,把構建產物(DLL,EXE,靜態內容等等)放到BuildArtifacts目錄下。

3. 運行單元測試。

由於示例應用叫作HelloCI,因而這個腳本也就命名爲HelloCI.msbuild。先添加命名空間,而後就能夠添加第一個Target了,我管它叫作Init。

<Target Name="Init">
    <MakeDir Directories="BuildArtifacts" />
</Target>

這個Target會調用MakeDir Task建立一個新的目錄,名叫BuildArtifacts,跟腳本在同一目錄下。運行腳本,你會發現該目錄被成功建立。若是再次運行,MSBuild就會跳過這個Task,由於同名目錄已經存在了。

接下來寫一個Clean Target,它負責刪除BuildArtifacts目錄和裏面的文件。

<Target Name="Clean">
    <RemoveDir Directories="BuildArtifacts" />
</Target>

理解了Init以後,這段腳本就應該很好懂了。試着執行一下,BuildArtifacts目錄應該就被刪掉了。下面再來把代碼中的重複幹掉。在Init和Clean兩個Target裏面,咱們都把BuildArtifacts的目錄名硬編碼到代碼裏面了,若是將來要修改這個名字的話,就得同時改兩個地方。這裏能夠利用Item或Property避免這種問題。

Item和Property只有些許差異。Property由簡單的鍵值對構成,在腳本執行的時候還能夠用 /property 賦值。Item更強大一些,它能夠用來存儲更復雜的數據。咱們這裏不用任何複雜數據,但須要用Items獲取額外的元信息,例如文件全路徑。

接下來修改一下腳本,用一個Item存放路徑名,而後修改Init和Clean,讓它們引用這個Item。

<ItemGroup>
    <BuildArtifactsDir Include="BuildArtifacts\" />
</ItemGroup>

<Target Name="Init">
    <MakeDir Directories="@(BuildArtifactsDir)" />
</Target>
<Target Name="Clean">
    <RemoveDir Directories="@(BuildArtifactsDir)" />
</Target>

Item是在ItemGroup裏面定義的。在一個Project中能夠有多個ItemGroup元素,用來把有關係的Item分組。這個功能在Item較多的時候特別有用。咱們在ItemGroup裏定義了BuildArtifactsDir元素,並用Include屬性指定BuildArtifacts目錄。記得BuildArtifacts目錄後面要有個斜槓。最後,咱們用了@(ItemName)語法在Target裏面引用這個目錄。如今若是要修改目錄名的話,只須要改BuildArtifactsDir的Include屬性就行了。

接下來還有個問題要處理。在BuildArtifacts目錄已經存在的狀況下,Init是什麼事都不幹的。也是就說,在調用Init的時候磁盤上的已有文件還會被保留下來。這一點着實不妥,若是能每次調用Init的時候,都把目錄和目錄裏面的全部文件都一塊兒刪掉再從新建立,就能保證後續環節都在乾淨的環境下執行了。咱們當然能夠在每次調用Init的時候先手工調一下Clean,但給Init Target加一個DependsOnTargets屬性會更簡單,這個屬性會告訴MSBuild,每次執行Init的時候都先執行Clean。

<Target Name="Init" DependsOnTargets="Clean">
    <MakeDir Directories="@(BuildArtifactsDir)" />
</Target>

如今MSBuild會幫咱們在調Init以前先調Clean了。跟DependsOnTargets這個屬性所暗示的同樣,一個Target能夠依賴於多個Target,之間用分號分割就行

接下來咱們要編譯應用程序,把編譯後的結果放到BuildArtifacts目錄下。先寫一個Compile Target,讓它依賴於Init。這個Target會調用另外一個MSBuild實例來編譯應用。咱們把BuildArtifacts目錄傳進去,做爲編譯結果的輸出目錄。

<ItemGroup>
    <BuildArtifactsDir Include="BuildArtifacts\" />
    <SolutionFile Include="HelloCI.sln" />
</ItemGroup>

<PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
    <BuildPlatform Condition=" '$(BuildPlatform)' == '' ">Any CPU</BuildPlatform>
</PropertyGroup>

<Target Name="Compile" DependsOnTargets="Init">
    <MSBuild Projects="@(SolutionFile)" Targets="Rebuild" Properties="OutDir=%(BuildArtifactsDir.FullPath);Configuration=$(Configuration);Platform=$(BuildPlatform)" />
</Target>

上面的腳本作了幾件事情。

首先,ItemGroup添加了另外一個Item,叫作SolutionFile,它指向solution文件。在構建腳本中用Item或Property代替硬編碼,這算的是一個優秀實踐吧。

其次,咱們建立了一個PropertyGroup,裏面包含兩個Property:Configuration和BuildPlatform。它們的值分別是「Release」和「Any CPU」。固然,Property也能夠在運行時經過/property(簡寫爲/p)賦值。咱們還用了Condition屬性,它在這裏的含義是,只有當這兩個屬性沒有值的狀況下,才用咱們定義的數據給它們賦值。這段代碼實際上就是給它們一個默認值。

接下來就是Compile Target了,它依賴於Init,裏面內嵌了一個MSBuild Task。它在運行的時候會調用另一個MSBuild實例。在腳本中定義了這個被內嵌的MSBuild Task要操做的項目。在這裏,咱們既能夠傳入另一個MSBuild腳本,也能夠傳入.csproj文件(它自己也是個MSBuild腳本)。但咱們選擇了傳入HelloCI應用的solution文件。Solution文件不是MSBuild腳本,可是MSBuild能夠解析它。腳本中還指定了內嵌的MSBuild Task要執行的Target名稱:「Rebuild」,這個Target已經被導入到solution的.csproj文件中了。最後,咱們給內嵌的Task傳入了三個Property。

OutDir 編譯結果的輸出目錄
Configuration 構建(調試、發佈等)時要使用的配置
Platform 編譯所用的平臺(x8六、x64等)

給上面這三個Property賦值用的就是先前定義的Item和Property。OutDir Property用的是BuildArtifacts目錄的全路徑。這裏用了%(Item.MetaData) 語法。這個語法應該看起來很眼熟吧?就跟訪問C#對象屬性的語法同樣。MSBuild建立出來的任何Item,都提供了某些元數據以供訪問,例如FullPath和ModifiedTime。但這些元數據有時候也沒啥大用,由於Item不必定是文件。

Configuration和Platform用到了先前定義好的Property,語法格式是$(PropertyName)。在這裏能夠看到系統保留的一些屬性名,用戶不能更改。定義Property的時候請不要用它們。

這裏還有些東西值得提一下。用了Property之後,咱們能夠在不更改構建腳本的狀況下使用不一樣的Configuration或者BuildPlatform,只要在運行的時候用 /property 傳值進去就行。因此「msbuild HelloCI.msbuild /t:Compile /p:Configuration:Debug」這個命令會用Debug配置構建項目,而「msbuild HelloCI.msbuild /t:Compile /p:Configuration:Test;BuildPlatform:x86」會在x86平臺下使用Test配置。

如今運行Compile,就能夠編譯solution下的兩個項目,把編譯結果放到BuildArtifacts目錄下。在完成構建腳本以前,只剩下最後一個Target了:

<ItemGroup>
    <BuildArtifacts Include="BuildArtifacts\" />
    <SolutionFile Include="HelloCI.sln" />
    <NUnitConsole Include="C:\Program Files (x86)\NUnit 2.6\bin\nunit-console.exe" />
    <UnitTestsDLL Include="BuildArtifacts\HelloCI.Web.UnitTests.dll" />
    <TestResultsPath Include="BuildArtifacts\TestResults.xml" />
</ItemGroup>
<Target Name="RunUnitTests" DependsOnTargets="Compile">
    <Exec Command='"@(NUnitConsole)" @(UnitTestsDLL) /xml=@(TestResultsPath)' />
</Target>

ItemGroup裏如今又多了三個Item:

NUnitConsole指向NUnit控制檯運行器(console runner);

UnitTestDLL指向單元測試項目生成的DLL文件;

TestResultsPath是要傳給NUnit的,這樣測試結果就會放到BuildArtifacts目錄下。

RunUnitTests Target用到了Exec Task。若是有一個測試運行失敗,NUnit控制檯運行器會返回一個非0的結果。這個返回值會告訴MSBuild有個地方出錯了,因而整個構建的狀態就是失敗【這裏提一下,單元測試之類的第三方類庫要保證一直在項目中,不然在上傳至版本管理服務器上時,可能會被忽略致使後面的執行測試期間出現問題】

如今這個腳本比較完善了,用一個命令就能夠刪除舊的構建產物、編譯、運行單元測試:

C:\HelloCI\> msbuild HelloCI.msbuild /t:RunUnitTests

咱們還能夠給腳本設一個默認Target,就免得某次都要指定了。在Project元素上加一個DefaultTargets屬性,讓RunUnitTests成爲默認Target。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="RunUnitTests">

你還能夠建立本身的Task。這裏有個例子,AsyncExec【我是沒打開,可能被Q了】,它容許人們以異步的方式執行命令。好比有個Target用來啓動Web服務器,要是用Exec命令的話,整個構建都會停住,直到服務器關閉。用AsyncExec這個命令可讓構建繼續執行,不用等待命令執行結束。

本文的完整腳本能夠在這裏下載【或者下載個人】。

在接下來的文章中,我會講述如何配置Jenkins。咱們再也不須要手動運行命令來構建整個項目,Jenkins會檢測代碼庫,一旦有更新就會自動觸發構建。

 

參考:

用MSBuild和Jenkins搭建持續集成環境

相關文章
相關標籤/搜索