MSBuild的深入認識

原文地址:http://knightyzj.iteye.com/blog/463157

最近在從事自動構造工作的過程中,對MSBuild本身有了一些更加深入的認識。MSBuild不僅僅是一個構造工具,應該稱之爲擁有相當強大擴展能力的自動化平臺。

按照筆者現在的理解,MSBuild平臺的主要涉及到三部分:執行引擎、構造工程、任務。其中最核心的就是執行引擎,它包括定義構造工程的規範,解釋構造工程,執行“構造動作”;構造工程是用來描述構造任務的,大多數情況下我們使用MSBuild就是遵循規範,編寫一個構造工程;MSBuild引擎執行的每一個“構造動作”就是通過任務實現的 ,任務就是MSBuild的擴展機制,通過編寫新的任務就能夠不斷擴充MSBuild的執行能力。

所以這三部分分別代表了引擎、腳本和擴展能力。

1、構造工程(腳本文件)

先說說構造工程,只要通過Notepad打開任何一個VS2005(也就是支持CLR 2.0)下的C#工程(csproj)文件,就知道構造工程到底是怎麼回事了。

如果說腳本,我們立刻想到的是VBScript或者JavaScript,構造工程內描述的內容,和常見的腳本語言的源文件之間還是有蠻大差距的,爲什麼也稱之爲“腳本”呢?因爲筆者覺得沒啥區別。腳本不就是純文本形式保存,不經編譯解釋執行,可以實現一定邏輯分支的程序麼?

再看構造工程,在構造工程中我們我們可以定義和使用變量(通過Property/PropertyGourp/Item/ItemGroup等元素),可以使用條件分支(通過Choose/When/Otherwise等元素)、能夠在運行時給變量賦值(通過執行任務,獲取其返回類型參數的方式)、能夠定義執行塊(通過Target元素,相當於函數)、能夠進行異常處理(通過OnError元素)、還可以複用已有工程定義的內容(通過Import元素)。擁有這些能力和高級語言已經相差無幾了,所以筆者認爲構造工程不是描述性語言,而是腳本語言。

這裏還需要強調一點的是,項目級元素(Property)可以在<PropertyGroup>元素下定義,也可以在構造過程中作爲外部參數傳入(具體參見《MSBuild命令行參考》)。這是一個非常有用的特性,一般編譯時選擇配置項(Debug或者Release)就是利用這個特性實現的。

有關構造工程的編寫規範可以參考《MSBuild項目文件引用》。

2、執行引擎

接下來看執行引擎,通常我們使用下面的命令行開始執行構造:

MSBuild.exe <ProjectFile>

其中<ProjectFile>是前面提到的構造工程,也就是腳本文件,那麼MSBuild.exe就應當是執行引擎了。

沒錯,不過看一下源代碼就會發現MSBuild.exe非常簡單,其實主要做的工作就是命令行解析、構造環境的準備(如生成日誌記錄模塊準備一些全局變量),然後就是創建Microsoft.Build.BuildEngine.Engine類的實例,然後調用其BuildProjectFile方法來完成。所以真正的構造邏輯是在Microsoft.Build.Engine.dll中定義並且實現的。下面簡單的代碼就模擬了MSBuild.exe的工作。

C-sharp代碼 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using Microsoft.Build.BuildEngine;  
  5.    
  6. namespace BuildAProjectCS  
  7. {  
  8.     class Program  
  9.     {         
  10.         static void Main(string[] args)  
  11.         {  
  12.             // Instantiate a new Engine object  
  13.             Engine engine = new Engine();  
  14.             // Point to the path that contains the .NET Framework 2.0 CLR and tools  
  15.             engine.BinPath = @"c:\windows\microsoft.net\framework\v2.0.xxxxx";  
  16.             // Instantiate a new FileLogger to generate build log  
  17.             FileLogger logger = new FileLogger();  
  18.             // Set the logfile parameter to indicate the log destination  
  19.             logger.Parameters = @"logfile=C:\temp\build.log";  
  20.             // Register the logger with the engine  
  21.             engine.RegisterLogger(logger);  
  22.             // Build a project file  
  23.             bool success = engine.BuildProjectFile(@"c:\temp\validate.proj");  
  24.             //Unregister all loggers to close the log file  
  25.             engine.UnregisterAllLoggers();  
  26.             if (success)  
  27.                 Console.WriteLine("Build succeeded.");  
  28.             else  
  29.                 Console.WriteLine(@"Build failed. View C:\temp\build.log for details");  
  30.         }  
  31.     }  
  32. }  

具體的對象模型參見CLR類庫參考中的《Microsoft.Build.Framework命名空間》和《Microsoft.Build.BuildEngine命令空間》。

筆者簡單地分析了一下MSBuild.exe和Microsoft.Build.Engine.dll的源代碼,MSBuild的構造過程大致如下:

a) 先創建一個構造請求(BuildRequest,構造請求是用來記錄構造狀態的數據結構),創建完畢之後將構造請求投遞到請求隊列中。

b) 在執行模塊中,從請求隊列中獲取請求,然後開始處理。

c) 通過Project類加載構造工程,加載過程中檢查是Sulotion、VC工程還是其它語言的工程。如果是Solution的話,生成一個臨時的包裝工程,逐一構造Solution中包含的工程;如果是VC工程的話,也生成一個包裝工程,在這個工程中直接執行VCBuild任務來執行構造。否則直接通過XmlDocument加載項目文件,解析其中的元素,識別Property、Item、Target之類元素。

d) 工程解析完畢後按照Target的順序逐一執行。

e) 在執行Target的過程中先解析是否存在依賴的Target以及OnError子句(即產生錯誤時需要執行的Target)。

f) 先執行Target依賴的Target,然後通過TaskEngine執行本Target中的每一個任務。如果Target每一個任務都正確執行的話,那麼執行下一個Target;否則執行錯誤處理的Target。

g) 執行Task的過程就是實例化註冊爲Task的類,然後調用其Execute方法。

h) 所有Target執行完畢,則本次構造也執行完畢。

以上僅僅爲了便於理解概念進行的描述,實際的構造過程可能是考慮到多CPU以及內聯編譯,內部邏輯相當複雜,很多地方應用了Proxy模式。

3、任務(Task)

通過對執行引擎的描述可以發現執行引擎主要是維護執行流程以及記錄執行流程中各類變量(Property和Item),具體構造過程中的每一個動作,都是通過Task實現的。也就是說單靠Microsoft.Build.Engine.dll雖然可以加載並且解析構造工程,但是無法完成構造動作。之所以MSBuild能夠完成編譯、鏈接、創建目錄、複製文件等一系列工作,都是因爲在Microsoft.Build.Tasks.dll中實現了與之對應的一個個任務。具體請參考《MSBuild任務參考》以及CLR類庫參考中的《Microsoft.Build.Task命名空間

通過觀察MSBuild自帶的這些常用任務,可以發現其中分爲兩類:一類從ToolTask繼承,這類任務基本上就是直接調用外部二進制文件執行完成某個特定的動作,例如VCBuild和Exec等;另一類直接從Task繼承,是通過內部代碼邏輯完成特定動作,例如Copy和MSBuild等。那些直接執行外部文件的任務雖然功能強大,但是有比較大的侷限性,執行結果的反饋非常有限,通常只有ExitCode,很難獲得其內部執行的更多信息,例如日誌輸出或者操作影響的結果。

大部分情況下我們只需要一個Exec任務就能夠完成全部的構造動作,但是這樣做的結果和我們寫一個命令行的批處理文件沒什麼區別了。MSBuild平 臺和命令行批處理最大不同在於,它是一個更緊密的工作環境,任務之間通過一系列自定義的全局參數互相協同工作,比較靈活並且移植性高;同時共享日誌模塊統 一輸出執行過程中的各類信息,便於觀察和分析。而批處理中各個命令之間幾乎是完全孤立的,只能通過硬編碼的方式進行協同,協作能力比較差。

舉個最簡單的例子,編譯三個工程,然後複製編譯結果到目標目錄下。如果用批處理可能會寫成這個樣子:“編譯工程1、複製編譯結果、編譯工程2、複製結果、編譯工程3、複製結果”。而利用MSBuild就可以簡化很多工作,例如“申明需要編譯的工程(工程1、工程2、工程3、...)、編譯需要編譯的工程、複製編譯結果”。

順便說一句,MSBuild這個任務比較有趣,在內部直接調用了構造引擎的方法(IBuildEngine2.BuildPrejectFilesInParallel)。這個例子又一次告訴我們這個世界上許多看似強大的東西,其實什麼都沒有幹,只是因爲他們手中掌握了有效的資源。-^o^-

除了基礎的任務之外,任務還可以任意擴展,並且實現這種擴展非常方便。創建一個CLR 2.0以上的類庫工程,編寫實現了ITask接口的類,然後在構造工程中通過<UsingTask>元素註冊任務就可以使用了。例如:

Xhtml代碼 
  1. <UsingTask  
  2. TaskName="Microsoft.CompactFramework.Build.Tasks.PlatformVerificationTask"  
  3. AssemblyName="Microsoft.CompactFramework.Build.Tasks, Version=9.0.0.0,  
  4. Culture=neutralPublicKeyToken=b03f5f7f11d50a3a/>  
  5. ……  
  6. <Target Name="PlatformVerificationTask">  
  7. <PlatformVerificationTask PlatformFamilyName="$(PlatformFamilyName)"  
  8. PlatformID="$(PlatformID)" SourceAssembly="@(IntermediateAssembly)"  
  9. ReferencePath="@(ReferencePath)"  
  10. TreatWarningsAsErrors="$(TreatWarningsAsErrors)"  
  11. PlatformVersion="$(TargetFrameworkVersion)"/>  
  12. </Target>  

所以說MSBuild能夠成爲一個構造引擎,不是因爲有個叫MSBuild的Exe文件,也不是因爲腳本文件被稱之爲構造工程,而是因爲與之配套的Microsoft.Build.Task.dll中主要實現了主要是和構造相關的任務。換句話說如果提供和測試相關的任務庫的話,MSBuild也就是一個自動測試的平臺。

總之,MSBuild本身更趨向於一個自動化執行平臺,可以根據需求編寫不同的腳本文件來滿足不同的應用,當現有能力無法滿足時,通過編寫新的任務進行擴展。不僅限於構造,自動安裝、自動測試等都可以依賴這個平臺來實現。

發佈了79 篇原創文章 · 獲贊 8 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章