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


 http://www.infoq.com/cn/articles/MSBuild-1

你或其他人剛剛寫完了一段代碼,提交到項目的版本倉庫里面。但等一下,如果新提交的代碼把構建搞壞了怎么辦?萬一出現編譯錯誤,或者有的測試失敗了,或者代碼不符合質量標准所要求的底限,你該怎么辦?

最不靠譜的解決方案就是寄希望於所有人都是精英,他們根本不會犯這些錯誤。但如果真的出現了這些問題,我們就希望發現的越早越好。最好的方式就是只要有代碼提交,我們就有某種方式對它進行驗證。這就是持續集成的作用。

持續集成相關的工具有很多。最流行的要數一款基於Java的名叫Jenkins的工具。它提供了Web界面,用戶可以在界面上配置Job,每個Job都包含一系列的構建步驟。Jenkins可以完成開頭那個場景中所提到的所有驗證工作,它還能更進一步做自動化部署或者一鍵式部署。

Jenkins是由Sun的前員工開發的,它的根基是Java,但也可以用在非Java的項目里,比如PHP、Ruby on Rails、.NET。在.NET項目里,你除了Jenkins之外還要熟悉另一樣工具:MSBuild。

Visual Studio用MSBuild構建.NET項目。MSBuild所需的僅僅是一個腳本,在腳本中指定要執行的target。項目中的.csproj和.vbproj 文件都是MSBuild腳本。

在這篇文章中,我們會從頭開始,一步步完成一個屬於我們自己的MSBuild腳本。在它完成以后,我們只需要一個命令就可以刪除之前的構建產物,構建.NET應用,運行單元測試。后面我們還會配一個Jenkins Job,讓它從代碼庫中更新代碼,執行MSBuild腳本。最后還會配另一個Jenkins Job,讓它監聽第一個Job的結果,當第一步成功以后,它會把相關的構建產物復制出來,放到web服務器里啟動運行。

我們用一個ASP.NET MVC 3應用做例子,在VS里面創建ASP.NET MVC 3應用並選擇“application”模版就行。我們還要用一個單元測試項目來跑測試。代碼可以在這里下載

你好,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,它已經把該配的都配好了。

進入存放剛才那個腳本的目錄后,以文件名當作參數調用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">
    lt;Exec Command="echo Hello from MSBuild" />
</Target>

現在EchoGreeting Target有了一個Exec Task,它會執行Command屬性中定義的任何命令。再運行一次腳本,你應該能看到更多信息了。在大多數時候,MSBuild的輸出信息都很長,你可以用 /verbosity 開關來只顯示必要信息。不過無論怎樣,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

          編譯所用的平台(x86、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,它允許人們以異步的方式執行命令。比如有個Target用來啟動Web服務器,要是用Exec命令的話,整個構建都會停住,直到服務器關閉。用AsyncExec這個命令可以讓構建繼續執行,不用等待命令執行結束。

本文的完整腳本可以在這里下載

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

作者簡介

Mustafa Saeed Haji Ali居住在Somaliland的Hargeisa。他是個程序員,最常用的是ASP.NET MVC。Mustafa喜歡測試,喜歡用Javascript框架,如KnockoutJS、AngularJS、SignalR。他熱衷於傳播最佳實踐。
 

 

查看英文原文:Continuous Integration with MSBuild and Jenkins – Part 1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM