前言
很多人一談到 MSBuild,腦子里就會出現 “XML”、“只能用 VS 的屬性框圖形界面操作”、“可定制性和擴展性差” 和 “性能低” 等印象,但實際上這些除了 “XML” 之外完全都是刻板印象:這些人用著 Visual Studio 提供的圖形界面,就完全不愿意花個幾分鐘時間翻翻檔案去理解 MSBuild 及其構建程序,
另外,再加上 vcxproj (Visual C++ 專案)的默認 MSBuild 構建檔案寫得確實談不上好(默認只能專案粒度并行編譯,想要原始碼級并行編譯你得加錢),但這跟 MSBuild 本身沒有關系,單純是 Visual Studio 自帶的構建檔案沒支持罷了,
實際上,MSBuild 是一個擴展性極強、開源、跨平臺且構建管道中都是傳遞的物件的構建系統,包含結構化資訊處理和結構化日志輸出的支持;另外,還提供了完整的 .NET Runtime 供你呼叫里面任何的 API,甚至用 MSBuild 編程都不在話下,
本系列文章就來讓大家以新的視角重新認識一下 MSBuild,并借助 MSBuild 來構建自己的專案,
安裝和使用
MSBuild 的開源代碼倉庫:https://github.com/dotnet/msbuild ,另外,MSBuild 也支撐了整個 .NET 的構建流程,因此安裝 MSBuild 最簡單的方法就是安裝一個 .NET SDK,同樣也是開源和跨平臺的,
安裝好后,你就可以通過運行 dotnet msbuild 呼叫 MSBuild 了,當然,你也可以選擇從原始碼自行構建出一個 msbuild 可執行檔案來用,
注意事項
在本系列文章中,將會撰寫一個 build.proj 用來測驗 MSBuild,并且本文中涉及到的 MSBuild 呼叫都是直接運行 msbuild 來完成的,如果你是用安裝 .NET SDK 的方法來安裝 MSBuild 的話,則需要使用 dotnet msbuild 來呼叫 MSBuild,
一些基礎
MSBuild 的構建檔案中,主要分為以下幾個部分:
- 專案(Project)
- 屬性(Property)
- 項(Item)
- 任務(Task)
- 目標(Target)
- 匯入(Import)
專案
專案是 MSBuild 構建檔案的頂級節點,
<Project Sdk="...">
</Project>
可以用來引入 SDK 等元素,允許直接參考 SDK 中定義的構建檔案,這個我們以后再具體說,目前只需要知道 Project 是 MSBuild 的頂層節點即可,
我們目前不需要引入什么 SDK,因此新建一個 build.proj,在其中寫入以下代碼就行了:
<Project>
</Project>
屬性
屬性顧名思義,就是用來為 MSBuild 構建程序傳遞的引數,有多種方式可以定義屬性,
第一種方式是在構建的時候通過命令列引數 -property 或 -p 傳入,例如:
msbuild build.proj -property:Configuration=Release
這樣就傳入了一個名為 Configuration 的屬性,它的值是 Release,
還有一種方式是在構建檔案中撰寫:
<PropertyGroup>
<Configuration>Release</Configuration>
</PropertyGroup>
PropertyGroup 就是專門用來撰寫屬性的組,你可以在里面利用 XML 來設定屬性,
對屬性的參考可以使用 $ 來參考,例如:
<Foo>hello</Foo>
<Bar>$(Foo) world!</Bar>
這樣 Bar 的值就會變成 hello world!,另外要注意,通過命令列傳入的屬性值優先級比頂層 PropertyGroup 中定義的屬性更高,因此如果用戶呼叫了:
msbuild build.proj -property:Foo=goodbye
則此時 Bar 的值就變成了 goodbye world!,
屬性的計算順序是從上到下計算的,并且屬性在 MSBuild 的構建程序中也是最先計算的,MSBuild 中也有一些內置屬性可以直接使用,例如 MSBuildProjectFullPath 表示當前專案的檔案路徑等等,可以在 MSBuild 檔案中查閱,
項
項就是 MSBuild 構建程序中要用的集合物件了,你可以利用項來在 MSBuild 中定義你想使用的東西,
例如:
<ItemGroup>
<Foo Include="hello" />
</ItemGroup>
這樣就定義了一個叫做 Foo 的項,它包含了一個 hello,其中,ItemGroup 是專門用來撰寫項的組,
項之所以說是集合物件,因為它可以被理解為一個陣列,你可以在構建檔案中通過 Include、Update 和 Remove 來操作這個陣列,Include 用來添加一個元素,Exclude 用來排除一個元素,Remove 用來洗掉一個元素,Update 用來更新一個元素的元資料(metadata,至于元資料是什么我們稍后再說),計算順序同樣也是從上到下的,
比如:
<ItemGroup>
<Foo Include="1" />
<Foo Include="2" />
<Foo Include="3" />
<Foo Include="4" />
<Foo Remove="3" />
</ItemGroup>
就會得到一個項 Foo,它包含 1、2、4,
在 MSBuild 中,多個元素可以用 ; 分隔,因此也可以寫成:
<ItemGroup>
<Foo Include="1;2;3;4" />
<Foo Remove="3" />
</ItemGroup>
而 MSBuild 很貼心的為我們準備了一些通配符,用來快速添加項,例如 *、**、和 ?,分別用來匹配一段路徑中的零個或多個字符、零段或多段路徑以及一個字符,然后配合 Exclude 可以篩選掉你不想要的東西,例如:
<ItemGroup>
<Foo Include="**/*.cpp" Exclude="foo.cpp">
</ItemGroup>
就可以把當前目錄和子目錄中所有的 C++ 檔案都添加到 Foo 項中,但是不包含 foo.cpp,
什么叫做元資料呢?例如我們如果想給 Foo 附帶一個資料 X,那么可以這么寫:
<ItemGroup>
<Foo Include="1">
<X>Hello</X>
</Foo>
<Foo Include="2">
<X>World</X>
</Foo>
</ItemGroup>
這樣 Foo 中的 1 就帶了一個值為 Hello 的 X,而 Foo 中的 2 則帶了一個值為 World 的 X,這個 X 就是項元素的元資料,
如果再加一個:
<Foo Update="1">
<X>Goodbye</X>
</Foo>
則可以把 1 的 X 元資料更新為 Goodbye,
另外,我們可以通過 % 來從項上參考元資料,例如 %(Foo.X),
任務
任務是 MSBuild 真正要執行的東西,例如編譯、打包和下載檔案等等任務,可以由我們自行用 C# 或者 VB.NET 等語言實作,
關于任務的撰寫,我們將在以后進行介紹,這里只簡單介紹一下任務的使用,
MSBuild 也內置了很多任務,例如 Message 用來列印資訊、Warn 和 Error 分別用來產生警告和錯誤、Copy 和 Delete 分別用來復制和洗掉檔案、 MakeDir 用來創建目錄、Exec 用來執行程式以及 DownloadFile 用來下載檔案等等,具體的內置任務可以去 https://docs.microsoft.com/zh-cn/visualstudio/msbuild/msbuild-task-reference 查看,
例如我們想要列印資訊,那么可以利用 Message 任務來完成,根據 Message 任務的檔案,我們知道它有 Importance 和 Text 兩個引數,
比如我們想要列印一下 Hello,就可以這么寫:
<Message Text="Hello" />
任務需要在目標中使用,
目標
目標是一組任務的集合,我們簡單理解為:為了完成一個目標,需要執行一系列的任務,
例如:
<Target Name="Print">
<Message Text="Hello" />
</Target>
這樣我們就定義了一個叫做 Print 的目標,它用來輸出一個 Hello,
此時我們用 -target 或 -t 指定執行 Print:
msbuild build.proj -target:Print -verbosity:normal
將會輸出 Hello,注意 Message 的默認重要性為 normal,而 MSBuild 默認的日志詳細等級為 quiet,只輸出 high 或以上優先級的東西,因此我們指定 -verbosity:normal 讓 MSBuild 同樣把 normal 等級的日志也輸出出來,
目標之間可以用過 BeforeTargets 和 AfterTargets 來設定順序(但是相互之間沒有依賴),還可以使用 DependsOnTargets 來設定依賴,例如:
<Target Name="PrintBye" DependsOnTargets="PrintHello;PrintWorld">
<Message Text="Bye" Importance="high" />
</Target>
<Target Name="PrintHello">
<Message Text="Hello" Importance="high" />
</Target>
<Target Name="PrintWorld" AfterTargets="PrintWorld">
<Message Text="World" Importance="high" />
</Target>
執行構建 PrintBye:
msbuild build.proj -target:PrintBye
將會輸出:
Hello
World
Bye
可以通過在專案上設定 DefaultTargets 表示如果沒有通過命令列引數傳入目標則默認執行的目標,還可以設定 InitialTargets 來表示始終最先執行的目標,
此外,目標還支持設定 Inputs、Outputs 和 Returns,分別表示預計作為輸入的項、輸出的項和目標回傳值,前兩個用于快取和增量編譯,Returns 和 Outputs 用法基本相同,但是 Returns 不參與增量編譯,關于增量編譯我們以后再介紹,
我們可以利用 CallTarget 任務來呼叫一個目標,然后獲取呼叫的目標的 Outputs 的輸出,通過這種方式,我們不需要手動撰寫任務也能實作函式呼叫,例如:
<Target Name="Hello" Returns="$(Result)">
<Message Text="你好,$(Name)" Importance="high" />
<PropertyGroup>
<Result>和 $(Name) 打了招呼</Result>
</PropertyGroup>
</Target>
<Target Name="Build">
<PropertyGroup>
<MyResult>還沒呼叫結果</MyResult>
</PropertyGroup>
<Message Text="$(MyResult)" Importance="high" />
<CallTarget Targets="Hello">
<!-- 把 Hello 目標的輸出存到 MyResult 屬性中 -->
<Output TaskParameter="TargetOutputs" PropertyName="MyResult"/>
</CallTarget>
<Message Text="$(MyResult)" Importance="high" />
</Target>
我們執行構建:
msbuild build.proj -target:Build -property:Name=Bob
得到輸出:
還沒呼叫結果
你好,Bob
和 Bob 打了招呼
這個時候你可能會想,如果要給這個 Hello 目標像函式呼叫那樣傳入引數怎么辦?此時可以用 MSBuild 任務,通過 Properties 來傳遞屬性,多個屬性同樣是通過 ; 分隔,并且其中可以用 $、@ 等參考其他屬性和項:
<Target Name="Hello" Returns="$(Result)">
<Message Text="你好,$(Age) 歲的 $(Name)" Importance="high" />
<PropertyGroup>
<Result>和 $(Name) 打了招呼</Result>
</PropertyGroup>
</Target>
<Target Name="Build">
<PropertyGroup>
<MyResult>還沒呼叫結果</MyResult>
</PropertyGroup>
<Message Text="$(MyResult)" Importance="high" />
<MSBuild Targets="Hello" Properties="Age=18" Projects="build.proj">
<Output TaskParameter="TargetOutputs" PropertyName="MyResult"/>
</MSBuild>
<Message Text="$(MyResult)" Importance="high" />
</Target>
然后我們傳入一個 Age 屬性進去來呼叫構建:
msbuild build.proj -target:Build -property:Name=Bob -p:Age=18
這次將會得到輸出:
還沒呼叫結果
你好,18 歲的 Bob
和 Bob 打了招呼
匯入
匯入顧名思義,就是匯入其他的構建檔案,這樣我們就可以不需要在一個檔案中撰寫所有的構建配置了,
匯入很簡單,只需要在 Project 節點里加入:
<Import Project="foo.proj" />
即可把引入的構建檔案里的內容直接插入到所在的位置,
一點示例
截止到現在,我們已經了解了很多東西,那么我們綜合起來用一下,
首先,創建一個 build.proj,里面撰寫:
<Project InitialTargets="PrintName" DefaultTargets="PrintInfo">
<PropertyGroup>
<Name>Alice</Name>
</PropertyGroup>
<ItemGroup>
<Foo Include="1">
<X>Hello</X>
</Foo>
<Foo Include="2">
<X>World</X>
</Foo>
</ItemGroup>
<Target Name="PrintName" >
<Message Text="你好 $(Name)" Importance="high" />
</Target>
<Target Name="PrintInfo" DependsOnTargets="BeforePrint">
<Message Text="@(Foo) 的元資料是 %(X)" Importance="high" />
</Target>
<Target Name="BeforePrint">
<Message Text="即將輸出資料" Importance="high" />
</Target>
<Target Name="AfterPrint" AfterTargets="PrintInfo">
<Message Text="資料已經輸出" Importance="high" />
</Target>
</Project>
然后我們試著執行一下 MSBuild:
msbuild build.proj
將會輸出:
你好 Alice
即將輸出資料
1 的元資料是 Hello
2 的元資料是 World
資料已經輸出
條件
至此,你可能會覺得 MSBuild 里的一切都是線性的,然而要怎么表達邏輯關系呢?這個時候就要用到條件(Condition)了,
我們可以在任何地方使用 Condition 來控制是否計算或執行一個屬性、項、任務、目標和引入,也就是說你可以在任何你想要的地方通過 Condition 來進行條件的控制,
例如:
<PropertyGroup>
<Name>Alice</Name>
<IsDefaultName Condition=" '$(Name)' == 'alice' ">true</IsDefaultName>
<IsDefaultName Condition=" '$(Name)' != 'alice' ">false</IsDefaultName>
</PropertyGroup>
MSBuild 中允許我們進行字串比較,并且默認是不區分大小寫的,上述代碼中,如果構建的時候 Name 是 Alice,那么 IsDefaultName 就是 true, 否則是 false,
我們定義一個目標輸出一下看看:
<Target Name="Print">
<Message Text="你好,$(Name),是否默認名字:$(IsDefaultName)" Importance="high" />
</Target>
運行:
msbuild build.proj
得到輸出
你好,Alice,是否默認名字:true
而如果我們通過命令列傳入一個 -property:Name=Bob,則輸出就變成了:
你好,Bob,是否默認名字:false
另外,我們還可以使用 Choose、When 和 Otherwise 來根據 Condition 選擇 When 或者 Otherwise 下的內容,例如:
<Choose>
<When Condition=" '$(Name)' == 'Alice' ">
<PropertyGroup>
<Age>16</Age>
</PropertyGroup>
<ItemGroup>
<Files Include="Alice/**/*.*" />
</ItemGroup>
</When>
<When Condition=" '$(Name)' == 'Bob' or '$(Name)' == 'David' ">
<PropertyGroup>
<Age>18</Age>
</PropertyGroup>
<ItemGroup>
<Files Include="$(Name)/**/*.*" />
</ItemGroup>
</When>
<Otherwise>
<PropertyGroup>
<Age>20</Age>
</PropertyGroup>
<ItemGroup>
<Files Include="Other/**/*.*" />
</ItemGroup>
</Otherwise>
</Choose>
上面當 Name 是 Alice 的時候,將會選擇第一個 When 里的東西,而如果是 Bob 或者 David,則會選擇第二個 When 里的東西,否則選擇 Otherwise 里的東西,
條件將允許我們在構建程序中進行復雜的計算,并且控制整個構建流程,
任務錯誤處理
任務可能會發生錯誤,在 MSBuild 中,可以通過 Error 產生錯誤、Warn 產生警告;一些內置的任務(例如 Delete、Copy 等)也可能產生錯誤;對于自行撰寫的任務而言,也有其方式產生錯誤或者警告,
如果發生了錯誤,則構建默認會直接停止并以失敗告終,但這不能滿足所有需要,因此我們還可以在任務上利用 ContinueOnError 來控制發生錯誤后的行為:
- ErrorAndContinue:當任務失敗時繼續執行
- WarnAndContinue 或 true:當任務失敗時繼續執行,并且把該任務中的錯誤視為警告
- ErrorAndStop 或 false:當任務失敗時停止構建
例如,這次我們使用上面的例子,對非默認名字產生錯誤:
<Target Name="Print">
<Message Text="你好,$(Name),是否默認名字:$(IsDefaultName)" Importance="high" />
<Error Condition=" '$(IsDefaultName)' == 'false' " Text="發生錯誤了" />
</Target>
<Target Name="Build" DependsOnTargets="Print">
<Message Text="構建完了" Importance="high" />
</Target>
此時執行構建:
msbuild build.proj -target:Build
將會輸出
你好,Alice,是否默認名字:true
構建完了
而如果執行:
dotnet msbuid build.proj -target:Build -property:Name=Bob
則會輸出
你好,Bob,是否默認名字:false
build.proj(10,5): error : 發生錯誤了
但如果我們把構建代碼改成:
<Target Name="Print">
<Message Text="你好,$(Name),是否默認名字:$(IsDefaultName)" Importance="high" />
<Error Condition=" '$(IsDefaultName)' == 'false' " ContinueOnError="ErrorAndContinue" Text="發生錯誤了" />
</Target>
<Target Name="Build" DependsOnTargets="Print">
<Message Text="構建完了" Importance="high" />
</Target>
再執行上述命令,則會輸出:
你好,Bob,是否默認名字:false
build.proj(10,5): error : 發生錯誤了
構建完了
MSBuild 和 .NET 函式呼叫
MSBuild 允許我們直接呼叫 MSBuild 內置的或者 .NET 中的函式,呼叫方法為 [型別名]::方法名(引數...),例如:
<PropertyGroup>
<Foo>1</Foo>
<Foo Condition="[MSBuild]::IsOsPlatform('Windows')">2</Foo>
</PropertyGroup>
則 Foo 在 Windows 上為 2,而在其他系統上為 1,
屬性和項都有各自的 MSBuild 內置函式可以用,例如 Exists 和 HasMetadata 等等,具體可在 MSBuild 官方檔案上查閱:
- 屬性函式:https://docs.microsoft.com/zh-cn/visualstudio/msbuild/property-functions
- 項函式:https://docs.microsoft.com/zh-cn/visualstudio/msbuild/item-functions
有了這些,我們便可以利用 MSBuild 完成各種事情,
結構化日志
有時撰寫好了構建檔案之后,我們希望能夠查看整個構建流程或者失敗的原因等,這個時候文本的日志就不夠用了,在 MSBuild 中,我們有強大的結構化日志,
只需要構建的時候傳入一個 -bl 引數指定 binlog 的位置,MSBuild 就能在構建時為我們生成一個極其強大的結構化日志,例如使用 “一點示例” 小節中的例子:
msbuild build.proj -bl:output.binlog
然后就可以在 MSBuild 結構化日志查看器上查看我們的 output.binlog 了,這個查看器有網頁版和 Windows 客戶端版,因此無論在哪個平臺上都是可以用的:https://msbuildlog.com ,
利用 MSBuild 結構化日志查看器,我們將能夠從頭到尾詳細掌控整個構建流程,包括屬性和項是怎么計算出來的、目標為什么被跳過了、目標的執行結果和執行時長是多少、有哪些目標依賴關系以及目標都是來自哪個構建檔案的等等資訊一覽無遺,這樣非常有助于我們快速撰寫和診斷構建檔案,
小總結
MSBuild 依托于 .NET 運行時,利用 XML 來描述構建檔案,是一個無需守護行程(daemon)的非常強大的構建系統,
本文主要介紹了 MSBuild 的基本概念和撰寫方法,以及結構化日志的使用方法,
如果你厭煩了撰寫 CMakeLists.txt、Makefile 的那種難以除錯、檔案不全并且到處都是純字串處理的體驗,不如試試 MSBuild,將能快速寫出可靠的構建配置,加速你的開發,
在下一篇文章中,我們將來介紹快取、增量編譯、任務的撰寫以及并行編譯等,讓 MSBuild 的構建變得又快又省心,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/478186.html
標籤:.NET Core
下一篇:Centos7部署Redis集群
