MSBuild全称(Microsoft Build Engine),是用来生成.NET程序的平台。您可能不知道它,但是如果您在使用VS做开发,那么一定时时刻刻在使用它。因为是它在背后为你管理生成你的项目文件。当新建一个项目时,注意下项目文件夹中的*.*proj文件就是为MSBuild提供的,这是个文本文件,基于XML格式,里面包含有项目所包含的文件,生成配置,输出配置等信息。当把一个文件或者图片等添加到项目中,就会在这里添加一个描述,反之则删除一个描述信息;在项目属性页所做的配置也会在这里存储。
想去了解这个源于以前学WPF时的疑惑(当时就想从MSBuild下手了,一直没精力,拖到现在),因为不知道XAML为何就跑到生成的程序集,以及这个XAML标记最后变成什么,WPF是如何处理这些XAML标签与C#代码的?一般写代码时我都会清楚的知道这个代码最后经由编译器变成了什么,这样心里比较底。但是这个XAML,是看不透,看不透就心里堵得慌,不踏实,我比较喜欢刨根问底,所以就想到通过这个入口探个究竟。
MSBuild有四个基本块(属性、项、任务、目标):
一句话总结MSBuild的作用:利用配置信息对项目文件实施特定顺序的操作。
属性声明方式:
1 <?xml version="1.0" encoding="utf-8"?> 2 <!--根元素,表示一个项目--> 3 <!--DefaultTargets用于定默认执行的目标--> 4 <Project DefaultTargets="build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 5 <!--属性都要包含在PropertyGroup元素内部--> 6 <PropertyGroup> 7 <!--声明一个"linianhui"属性,其值为"hello world"--> 8 <linianhui>hello world</linianhui> 9 </PropertyGroup> 10 <!--目标--> 11 <Target Name="build"> 12 <!--MSBuild提供的一个内置任务,用于生成记录信息用$(属性名)来引用属性的值--> 13 <Message Text="$(linianhui)"></Message> 14 </Target> 15 </Project>
保存此文件到d:\helloworld.xml文件。打开CMD窗口,输入MSBuild helloworld.xml:
打印出“linianhui”属性的值。MSBuild提供一些保留属性,可以方便的引用$,如$(MSBuildProjectFile)将返回项目文件的完整名(helloworld.xml)。其他的保留属性可以查阅MSDN帮助文档。
项声明方式:
<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <!--项都要包含在ItemGroup元素内部--> <ItemGroup> <!--声明一个"CSFile"的项,Include表示引入"csfile1.cs"文件--> <CSFile Include="csfile1.cs"> <!--Version表示项的元数据(附加信息)--> <Version>1.0.0.0</Version> </CSFile> <!--也可用";"一次引入多个文件--> <CSFile Include="csfile2.cs;csfile3.cs"/> </ItemGroup> <Target Name="build"> <!--@引用项的值,默认以";"分割开--> <!--输出"csfile1.cs;csfile2.cs;csfile3.cs"--> <Message Text="@(CSFile)"></Message> <!--可以加第二个参数替换默认的";"分隔符--> <!--输出"csfile1.cs+csfile2.cs+csfile3.cs"--> <Message Text="@(CSFile,'+')"></Message> <!--%引用项的元数据,输出"1.0.0.0"--> <Message Text="%(CSFile.Version)"></Message> </Target> </Project>
上述Msaage就是一个任务,用于打印信息,常用的一些还包括CSC、MakeDir、Copy等等,大多任务都是有输出信息的,这些信息可以通过OutPut元素存储在属性或者项中。先写如下CS代码:
1 //存为d:\MSBuildDemo.cs 2 public class MSBuildDemo 3 { 4 static void Main() 5 { 6 System.Console.WriteLine("MSBuild组织编译"); 7 } 8 }
然后更改项目文件如下:
1 <?xml version="1.0" encoding="utf-8"?> 2 <Project DefaultTargets="build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 3 <ItemGroup> 4 <!--指定要编译的文件--> 5 <CSFile Include="MSBuildDemo.cs"/> 6 </ItemGroup> 7 <Target Name="build"> 8 <!--使用Csc任务,对应csc编译器--> 9 <!--Sources属性表示要编译的文件集合--> 10 <!--TargetType表示编译目标类型,对应csc编译器的/target参数--> 11 <Csc Sources="@(CSFile)" 12 TargetType="exe"> 13 <!--OutputAssembly为csc的输出参数--> 14 <!--PropertyName表示把TaskParameter属性所指定的输出参数的值存储到outputExeName这个属性中--> 15 <!--Output还有一个ItemName属性,表示存储到一个项中--> 16 <Output TaskParameter="OutputAssembly" PropertyName="outputExeFileName"/> 17 </Csc> 18 <!--Message任务就可以使用csc所导出的属性outputExeFileName了--> 19 <!--输出MSBuildDemo.exe--> 20 <Message Text="$(outputExeFileName)"/> 21 <!--Exec任务可以运行带有指定程序(可加参数)或命令--> 22 <!--运行刚从MSBuildDemo.cs源文件编译好的程序--> 23 <!--运行结果为"MSBuild组织编译"--> 24 <Exec Command="$(outputExeFileName)"></Exec> 25 </Target> 26 </Project>
用MSbuild执行此项目文件,如期正确打印信息。
上面的一个例子中Target元素就是MSBuild目标,此目标按照编译源代码、打印编译好的程序文件名、执行该文件这个顺序组织了这三个任务。这就是目标所要做的事情。先简单介绍到这里吧,关于(属性、项、任务、目标)的一些扩展信息会在下一篇介绍。如有错误之处,欢迎指正!
在上一篇简单的介绍了下MSBuild中的四个基本块,每块介绍比较单薄,在这里对在大多数的项目模版生成的*.*proj文件中比较常见一些用法和概念做些补充。主要有一下几方面:
一些字符在MSBuild中代表着特殊的上下文含义,如下:
针对MSBuild的特殊字符转义需要用[%xx]这种方式,xx代表字符的ASCII十六进制值([%=%25][$=%24][@=%40]['=%27][;=%3B][?=%3F][*=%2A])。针对XML保留字符则使用<这种方式。 一般用到这些特殊字符的情况不多,见到时能知道是转义就可以了。
条件在*.*proj项目文件中非常常见,用Condition特性来表示一个布尔表达式,类似于if条件,几乎所有的元素都可以具有Conditon特性。一个简单的例子如下:
1 <?xml version="1.0" encoding="utf-8"?> 2 <!--condition.xml文件--> 3 <Project DefaultTargets="show" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 4 <PropertyGroup> 5 <!--Condition在属性、项、任务、目标生都有使用--> 6 <!--如果Configuration为空(''),则其值为Debug--> 7 <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> 8 <Platform Condition=" '$(Platform)' == '' ">x86</Platform> 9 </PropertyGroup> 10 <ItemGroup> 11 <!--如果csfile1.cs文件存在就包含在CSFile项中--> 12 <CSFile Include="csfile1.cs" Condition="Exists('csfile1.cs')"></CSFile> 13 </ItemGroup> 14 <Target Name="show"> 15 <!--输出Debug|x86--> 16 <Message Text="$(Configuration)|$(Platform)"/> 17 <!--输出空,因为csfile1.cs并不存在--> 18 <Message Text="@(CSFile)"/> 19 </Target> 20 </Project>
还有一些常用的表达式如!=、!、And、Or等。
上篇介绍到可以用$可以引用自定义的属性,除此之外亦可以引用系统的环境变量,如$(Path),以及 MSBuild保留属性(MSDN)。
属性除了可以在项目文件中声明是赋值外,在MSBuild命令行也允许设置属性的值(语法:/p:propertyName=value)。称作全局属性,这类属性会重写在项目文件中设置的属性值,保留属性除外的任何属性都可被这种方式覆盖其原值。 以上面示例为基础:[MSBuild condition.xml /p:Platform=x64],则最终输出结果就为Debug|x64了。
属性还有一种叫做任务发出属性,在上篇用到了,由Output元素的PropertyName特性指定了属性名,这类属性不像一般的声明式属性那样赋值,而是动态得到的值。是在项目文件中很常见的用法。
项大都是用来引入文件用的,而文件会有一些附加信息,比如版本,语言等,而这些附加信息在项目文件中是以项的子元素的出现的,称为项的元数据。元数据是键/值的形式存储的,声明方式和属性相同。
1 <ItemGroup> 2 <!--如果csfile1.cs文件存在就包含在CSFile项中--> 3 <CSFile Include="csfile1.cs"> 4 <!--声明元数据,必须为项的一级子元素--> 5 <!--引用方式:%(CSFile.Culture)--> 6 <Culture>zh-cn</Culture> 7 </CSFile> 8 </ItemGroup>
除了自定义的一些元数据外,系统还提供一些隐式存在的元数据,即不用声明即可使用,具体可参见MSBuild常见的已知元数据。引用这类元数据的语法和自定义的完全相同。
项转换允许把一个项的列表与另一个列表一一变换。比如下面的例子:
1 <?> 5 <CSFile Include="1.cs;2.cs"/> 6 <!--%(Filename)为项的元数据,由系统提供--> 7 <VBFile Include="@(CSFile->'%(Filename).vb')"/> 8 </ItemGroup> 9 <Target Name="show"> 10 <!--输出1.cs;2.cs--> 11 <Message Text="@(CSFile)"/> 12 <!--输出1.vb;2.vb--> 13 <Message Text="@(VBFile)"/> 14 </Target> 15 </Project>
从上篇中我们对任务的认识是它是一个原子操作,用来执行某一项逻辑处理,但是xml格式的项目文件是没有这个处理能力的,所以这些任务都是映射到.NET类库中的一些类,由这些类来处理操作中的逻辑。具体来说都是实现ITask接口的类,ITask接口位于Microsoft.Build.Framework命名空间。当然我们也可以实现自己的任务类,直接实现ITask接口或者继承自Task(此抽象类实现了ITask接口的部分功能,可简化自定义任务类的编写,留出一个Execute抽象方法供子类重写自己的任务逻辑)。然后通过UsingTask元素映射到出一个任务元素。我就继承Task写一个简单的示例:
1 //AddTwoNumberTask.cs,需编译为dll 2 using System; 3 using Microsoft.Build.Utilities; 4 using Microsoft.Build.Framework; 5 6 /// <summary> 7 /// 继承Task,任务逻辑是处理加法 8 /// </summary> 9 public class AddTwoNumberTask : Task 10 { 11 /// <summary> 12 /// 定义一个加数, 13 /// 如果一个输入属性被要求必须输入,则用[Required]特性标识该属性 14 /// </summary> 15 public String Number1 { get; set; } 16 /// <summary> 17 /// 定义另一个加数 18 /// </summary> 19 public String Number2 { get; set; } 20 public override bool Execute() 21 { 22 this.Sum = (Int32.Parse(this.Number1) + Int32.Parse(this.Number2)).ToString(); 23 return true; 24 25 } 26 /// <summary> 27 /// 定义一个输出参数,使用Output特性修饰该属性 28 /// </summary> 29 [Output] 30 public String Sum { get; set; } 31 }
1 <!--buildAddTaskDll.csproj--> 2 <?xml version="1.0" encoding="utf-8"?> 3 <!--从AddTwoNumberTask.cs源文件到编译成dll--> 4 <Project DefaultTargets="buildAddTaskDll" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 5 <PropertyGroup> 6 <OutputType>Library</OutputType> 7 </PropertyGroup> 8 <ItemGroup> 9 <Reference Include="Microsoft.Build.Framework" /> 10 <Reference Include="Microsoft.Build.Utilities.v4.0" /> 11 <Reference Include="System" /> 12 </ItemGroup> 13 <ItemGroup> 14 <Compile Include="AddTwoNumberTask.cs" /> 15 </ItemGroup> 16 <Target Name="buildAddTaskDll"> 17 <!--@(Reference->'$(MSBuildBinPath)\%(Identity).dll')表示项转换--> 18 <Csc Sources="@(Compile)" 19 References="@(Reference->'$(MSBuildBinPath)\%(Identity).dll')" 20 TargetType="$(OutputType)"> 21 </Csc> 22 </Target> 23 </Project>
用MSBuild编译buildAddTaskDll.csproj项目文件。得到AddTwoNumberTask.dll程序集。再编写一个项目文件usingtask如下:
<?xml version="1.0" encoding="utf-8"?> <!--使用自定义的任务做加法--> <Project DefaultTargets="show" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <UsingTask TaskName="AddTwoNumberTask" AssemblyFile = "AddTwoNumberTask.dll"/> <Target Name="show"> <AddTwoNumberTask Number1="1" Number2="2" > <Output TaskParameter="Sum" PropertyName="SumValue"/> </AddTwoNumberTask> <!--输出3--> <Message Text="$(SumValue)"/> </Target> </Project>
如果仔细看AddTwoNumberTask.cs文件就会发现
//如果Number1或者2不是数字,则此任务就会抛异常了 this.Sum = (Int32.Parse(this.Number1) + Int32.Parse(this.Number2)).ToString();
那么如果<AddTwoNumberTask Number1="1" Number2="2" >在这里加一个ContinueOnError=“true”,则表示会忽略掉逻辑处理中的错误,继续运行,否则会终止执行后续任务。如果任务有输出参数的话,Output元素总是作为任务的子元素出现,作为一个中间桥梁把任务的输出传输到属性或者项中。
Project根元素代表者一个项目文件,上面的例子我都会写一个DefaultTargets特性来指定该项目文件要执行的默认目标是哪一个。其实此特性是可选的,也是可以用分号分割写多个的,执行顺序依据书写顺序来判定,也可通过MSBuild命令行参数来传递:
msbuild /target:Build1;Build2
除此之外,Project元素还有一个可选特性InitialTargets,也支持多个目标。如果这两个特性都没有,则MSBuild先执行它遇到的第一个Target。Target有一个DependsOnTargets特性表示当前目标依赖另一个目标,效果就是DependsOnTargets特性指定的目标先于当前目标执行。这绕来绕去好多先后顺序关系,写一个示例看看吧。
1 <?xml version="1.0" encoding="utf-8"?> 2 <!--目标执行顺序--> 3 <!--如果InitialTargets特性存在,则首先执行此目标列表--> 4 <!--如果DefaultTargets特性存在,则继续执行此目标列表--> 5 <Project InitialTargets="B1;B2" DefaultTargets="B3;B4" 6 ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> 7 <!--如果发现Target具有DependsOnTargets特性--> 8 <!--则先执行DependsOnTargets指定的目标--> 9 <!--MSBuild4新加入了RunBeforeTargets和RunAfterTargets特性--> 10 <!--其作用和DependsOnTargets类似,一前一后,不做演示了--> 11 <Target Name="B1" DependsOnTargets="B5"> 12 <Message Text="B1"/> 13 </Target> 14 <Target Name="B2"> 15 <Message Text="B2"/> 16 </Target> 17 <Target Name="B3"> 18 <Message Text="B3"/> 19 </Target> 20 <Target Name="B4"> 21 <Message Text="B4"/> 22 </Target> 23 <Target Name="B5"> 24 <Message Text="B5"/> 25 </Target> 26 <!--结果为:B5 B1 B2 B3 B4--> 27 </Project>
项目模版产生的*.*proj项目文件大量的使用这个元素,用来导入可重用的项目文件,其中最常见的一个应该是这个吧,如果你用C#开发的话。
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
MSBuildToolsPath或者是MSBuildBinPath,Project特性指定要导入的项目文件。Import元素像是一个占位元素,MSBuild在执行到此时会用*.targets替换掉此元素,就像本来就声明在这里一样,所以和*.targets文件有关的所有保留属性会被重置。 Import元素对导入文件的扩展名无要求,文件是正确的项目文件就行,但一般约定为*.targets。
了解了以上知识点后,阅读一般的项目模版生成的项目文件(*.*proj)应该是可以的了,下篇文章先认识几个重要的*.targets,为剖析项目文件做准备。
备注:针对项目文件中所指的“特性”是表示一个xml元素的“属性”。由于属性在MSBuild中有特殊含义,则MSDN文档一律把项目文件中的xml属性称作是特性,比如Message任务的Text特性。如有错误之处,欢迎指正!