MSBuild入門

- 中國WEB開發者網絡 (http://www.webasp.net)
-- 技術教程 (http://www.webasp.net/article/)
--- MSBuild入門 (http://www.webasp.net/article/9/8387.htm)
-- 作者:未知
-- 發佈日期: 2004-03-23
如果你和我一樣一直都在用NAnt管理生成過程的話,那麼你一定會高度關注MSBuild。原因很簡單,因為它屬於微軟,你可以不喜歡它,但你一定要學會用它。



在熬過了幾個晚上以後,我終於讓自己適應了MSBuild的語法。這可真不容易,特別是當自己已經習慣了NAnt的小寫規範之後。不過這不成問題,因為隨著自己對MSBuild的理解一點點加深,自己還真的喜歡上它了。



好吧,下面就讓我來簡單地介紹一下我在學習MSBuild使用過程中的一點經驗。如果你還在MSBuild的大門外徘徊,那麼希望這篇東西能帶你進入那扇門。



準備工作

首先要提到的是有關如何使用MSBuild的一些重要資源。它們是:



1. Alex Kipman的MSDNTV Show:

http://msdn.microsoft.com/msdntv/episode.aspx?xml=episodes/en/20040122VSNETAK/manifest.xml

2. Alex Kipman和Rajeev Goel在PDC2003上的演講:

http://microsoft.sitestream.com/PDC2003/TLS/TLS347.htm

上面這兩項出自MSBuild開發組的Alex Kipman,從理論上說他應該是瞭解MSBuild的第一人,他給出的幾個演示的確給了我非常大的幫助。(不過我非常不喜歡他的聲音,又尖又細。)



3. MSBuild Doc

http://msdn.microsoft.com/longhorn/toolsamp/default.aspx

這是最重要的,其中包括Alex Kipman主筆的五份重要文檔:MSBuildFileFormat、MSBuildWalkthrough、MSBuildTasks、HowToWriteATask以及MSBuildCommandLine。這可能是目前情況下外界能獲得的有關MSBuild最詳細的文檔。





Demo

好了,一切準備工作就緒,讓我們以一個簡單的示例開始吧。



首先寫一個簡單的C# Console程序(你也可以把它改成VB.NET):



// HelloMSBuild.cs

using System;



class HelloMSBuild

{

public static void Main()

{

Console.WriteLine("Hello MSBuild!");

}

}



下面我們就要寫一個.csproj文件來控制整個生成過程。值得注意的是,如果在調用MSBuild.exe時沒有指定具體的項目文件,MSBuild引擎會在當前目錄下查找一個名為*.*proj的項目文件。如果你在同一目錄中寫了多個這樣的項目文件,那麼需要手動指定MSBuild.exe的目標文件,方法是:



MSBuild a.csproj



否則MSBuild會提示出錯,要求你手動指定目標項目文件。



以下是項目文件:



<!-- Build.csproj -->

<Project DefaultTargets="Run">



<Property Bin="bin" />

<Property OutputAssembly="HelloMSBuild" />



<Item Type="Source" Include="HelloMSBuild.cs" />



<Target Name="Build">

<Task Name="MakeDir"

Directories="$(Bin)"

Condition="!Exists('$(Bin)')" />

<Task Name="Csc"

Sources="@(Source)"

TargetType="exe"

OutputAssembly="$(Bin)\$(OutputAssembly).exe" />

</Target>



<Target Name="Run" DependsOnTargets="Build">

<Task Name="Exec"

Command="$(Bin)\$(OutputAssembly).exe" />

</Target>

</Project>



如果你此前沒有過NAnt的開發經驗,那麼上面這些東西肯定看起來挺嚇人。這個時候最好的辦法是打開那篇MSBuildFileFormat,對照上面代碼查找相應的項目元素的含義。下面我對其中重要的項目元素進行一下解釋。



1. Project元素。這是每一個項目文件的最外層元素,它表示了一個項目的範圍。如果缺少了這一元素,MSBuild會報錯稱Target元素無法識別或不被支持。



Project元素擁有多個屬性,其中最常用到的是DefaultTargets屬性。我們都知道,在一個項目的生成過程中可能需要完成幾項不同的任務(比如編譯、單元測試、check-in到源代碼控制服務器中等),其中每一項任務都可以用Target來表示。對於擁有多個Target的項目,你可以通過設置Project的DefaultTargets(注意是複數)屬性來指定需要運行哪(幾)個Target,比如:



<Project DefaultTargets=」Build」 >

...



或者:



<Project DefaultTargets=」Build;Test;Run」 >

...



如果沒有這個設置,MSBuild將只運行排在最前面的那個Target。



2. Property元素。在項目中你肯定需要經常訪問一些信息,例如需要創建的路徑名、最終生成的程序集名稱等。這些信息你最好別hard code進項目中,除非你一次寫過之後永不更改。這時Property就能派上用場了。你把上面提到的那些信息以name/value的形式添加進Property,隨後就可以以$(PropertyName)的形式訪問。這樣你就無須為了改動一個文件名稱而讓整個項目文件傷筋動骨了。比如上面代碼中的Bin就是將要創建的路徑名稱,而AssemblyName則是最終要生成的程序集名稱。這些屬性的名稱不是固定的,你完全可以按自己的習慣來進行命名。在使用時,你需要把屬性名稱放在」$(「和」)」對內(不包括引號),以表示這裡將被替換成一個Property元素的值。



另外,如果Property元素數量比較多,你還可以把它們分門別類地放在不同的PropertyGroup裡,以提高代碼的可閱讀性。這對Property本身沒有任何影響。比如:



<PropertyGroup>

<Property ... />

<Property ... />

</PropertyGroup>



3. Item元素。在整個項目文件中你肯定要提供一些可被引用的輸入性資源(inputs)信息,比如源代碼文件、引用的程序集名稱、需要嵌入的圖標資源等。它們應該被放在Item裡,以便隨時引用。語法是:



<Item Type=」TheType」 Include=」NameOrPath」 />



其中Type屬性可以被看作是資源的類別名稱,比如對於.cs源文件,你可以把它們的Type都設置為Source,對於引用的程序集把Type都設置為Reference,這樣在隨後想引用這一類別的資源時只要引用這個Type就可以了,方法是@(TypeName)。可千萬別和Property的引用方法弄混了。



既然Type是資源的類名,那麼Include就是具體的資源名稱了,比如在上面的示例代碼中,Include引用的就是C#源代碼文件的名稱。你也可以用使用通配符*來擴大引用範圍。比如下面這行代碼就指定了當前目錄下的所有C#文件都可以通過@(Source)來引用:



<Item Type=」Source」 Include=」*.cs」 />



另外,你也可以通過與PropertyGroup類似的方法把相關的Item放在ItemGroup裡。



4. Target元素。上面已經提到了,Target表示一個需要完成的虛擬的任務單元。每個Project可以包括一個或多個Target,從而完成一系列定制的任務。你需要給每個Target設置一個Name屬性(同一Project下的兩個Target不能擁有同樣的Name)以便引用和區別。



舉例來說,在你的項目生成過程中可能需要完成三個階段的任務:首先從VSS中check-out源代碼,接下來編譯這些代碼並執行單元測試,最後把它們check-in回VSS。那麼通常情況下你可以創建三個不同的Target以清晰劃分三個不同的階段:



<Target Name=」CheckOut」 >

...

</Target>



<Target Name=」Build」 DependsOnTargets=」CheckOut」>

<Task Name=」Build」 .../>

<Task Name=」UnitTest」 ... />

</Target>



<Target Name=」CheckIn」 DependsOnTargets=」CheckOut;Build」>

...

</Target>



這樣,你就可以非常清晰地控制整個生成過程。為了反應不同Target之間的依賴關係(只有Check-in後才能編譯,只有編譯完成才可能Check-out……),你需要設置Target的DependsOnTargets屬性(注意是複數),以表示僅當這些Target執行完成之後才能執行當前的Target。當MSBuild引擎開始執行某項Target時(別忘了Project的DefaultTargets屬性),會自動檢測它所依賴的那些Target是否已經執行完成,從而避免因為某個生成環節缺失而導致整個生成過程發生意外。



你可以通過Project的DefaultTargets屬性指定MSBuild引擎從哪(幾)個Target開始執行,也可以在調用MSBuild.exe時使用t開關來手動指定將要運行的Target,方法如下:



MSBuild /t:CheckOut



這樣,只有CheckOut(以及它所依賴的Target,在上文中沒有)會被執行。



5. Task元素。這可能是整個項目文件中最重要的,因為它才是真正可執行的部分(這也是為什麼我在上面說Target是虛擬的)。你可以在Target下面放置多個Task來順序地執行相應的任務,比如我在上面示例代碼中就在兩個不同的Target中安排了MakeDir、Csc和Exec三個不同的Task。這些Task通過Name屬性來相互區分,並各自擁有不同的其它屬性來完成不同的任務,比如Csc有Sources(源代碼文件)、TargetType(目標類型)、OutputAssembly(生成程序集名稱)等屬性,而MakeDir則只需設置Directories(需要創建的路徑名稱列表)即可。



也許你會奇怪這些Task的名稱和屬性從哪裡來。好吧,請用文本編譯器打開%windir%\Microsoft.NET\Framework\v1.2.30703\Microsoft.BuildTasks文件,看到了嗎?默認情況下裡面應該是這樣的(不同的版本可能會有細微差別):



<!-- This file lists all the tasks that ship by default with MSBuild -->

<DefaultTasks>

<UsingTask TaskName="Microsoft.Build.Tasks.Csc" AssemblyName="MSBuildTasks"/>

<UsingTask TaskName="Microsoft.Build.Tasks.MSBuild" AssemblyName="MSBuildTasks"/>

<UsingTask TaskName="Microsoft.Build.Tasks.Exec" AssemblyName="MSBuildTasks"/>

<UsingTask TaskName="Microsoft.Build.Tasks.Vbc" AssemblyName="MSBuildTasks"/>

<UsingTask TaskName="Microsoft.Build.Tasks.MakeDir" AssemblyName="MSBuildTasks"/>

<UsingTask TaskName="Microsoft.Build.Tasks.ResGen" AssemblyName="MSBuildTasks"/>

<UsingTask TaskName="Microsoft.Build.Tasks.Copy" AssemblyName="MSBuildTasks"/>

<UsingTask TaskName="Microsoft.Build.Tasks.NetAssemblyResolver" AssemblyName="MSBuildTasks"/>

<UsingTask TaskName="Microsoft.Build.Tasks.TransformPath" AssemblyName="MSBuildTasks"/>

</DefaultTasks>



你會注意到,在DefaultTasks元素下面排列的全是UsingTask,其中指明每一個Task的TaskName(名稱)和AssemblyName(程序集)。比如說第一個UsingTask就對應著我們上面用過的Csc任務,它的完整名稱(namespace+class)是Microsoft.Build.Tasks.Csc,位於MSBuildTasks.dll程序集中(請在同一目錄下確認這一.dll文件的存在)。這樣,MSBuild引擎在遇到對Csc任務的調用時就會通過這裡的註冊信息來確定Csc所在的程序集,從而最終運行相應的托管代碼。這樣,如果你自己也寫了不同的Task,請按同樣的方式對它進行註冊以便使用。如果你引用了一個還沒有註冊的Target,那麼MSBuild引擎將無法找到它的存在而導致生成失敗。



當然,MSBuild Task的註冊方式不止以上一種。以上註冊方法的影響範圍是全局,你可以在每一個Project裡應用上面註冊的那些Task。但你也可以選擇在Project範圍內註冊Task,這將對應著另外一種略有不同的方法。我會在後面的一篇文章裡給出具體介紹。在這裡,你只需明白你所需要的Task在哪裡找到,而它們的具體用法可以通過參考MSBuildTasks一文來獲得,在這裡我就不細說了。



OK,介紹了一長串,還是快點把我們的Build.csproj運行起來吧。請在shell的同一目錄下輸入以下命令:



MSBuild



或者:



MSBuild Build.csproj



運行結果如下:



d:\Dev\MyMSBuildDemo>msbuild Build.csproj

msbuild Build.csproj

Microsoft (R) .NET Build Engine version 1.2.30703.4

[Microsoft .Net Framework, Version 1.2.30703.4]

Copyright (C) Microsoft Corporation 2003. All rights reserved.



Target "Build" in project "Build.csproj"

Task "MakeDir"

Creating directory "bin".

Task "Csc"

Csc.exe /out:"bin\HelloMSBuild.exe" /target:exe "HelloMSBuild.cs"



Target "Run" in project "Build.csproj"

Task "Exec"

Hello MSBuild!



可見,在Build.csproj指定的兩個Target和三個Task均按相應的順序依次運行,在Csc執行時MSBuild還顯示出了當前執行的具體命令,而在原來的Visual Studio .NET年代,你是無法獲知當前正在執行的編譯命令是什麼(據Alex Kipman稱,連Visual Studio .NET自己也不知道正在執行的具體命令,因為那些命令已經被hard code進了「黑盒子」,根本無法提取)。



好了,一個簡單的MSBuild文件用法示例就到這兒了。如果你此前還沒接觸過MSBuild或者NAnt,那麼希望這篇文章能讓你對MSBuild的用法有個初步的瞭解。還有很多的細節我在文中沒有涉及,如果你感興趣的話就請下載前面我提到的那些MSBuild文檔來自己研究吧。我會在下一篇文章裡介紹如何開發自己的MSBuild Task。



webasp.net