前言

很多人一谈到 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 是专门用来编写项的组。

项之所以说是集合对象,因为它可以被理解为一个数组,你可以在构建文件中通过 IncludeUpdateRemove 来操作这个数组,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 用来打印信息、WarnError 分别用来产生警告和错误、CopyDelete 分别用来复制和删除文件、 MakeDir 用来创建目录、Exec 用来执行程序以及 DownloadFile 用来下载文件等等,具体的内置任务可以去 https://docs.microsoft.com/zh-cn/visualstudio/msbuild/msbuild-task-reference 查看。

例如我们想要打印信息,那么可以利用 Message 任务来完成,根据 Message 任务的文档,我们知道它有 ImportanceText 两个参数。

比如我们想要打印一下 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 等级的日志也输出出来。

目标之间可以用过 BeforeTargetsAfterTargets 来设置顺序(但是相互之间没有依赖),还可以使用 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 来表示始终最先执行的目标。

此外,目标还支持设置 InputsOutputsReturns,分别表示预计作为输入的项、输出的项和目标返回值,前两个用于缓存和增量编译,ReturnsOutputs 用法基本相同,但是 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 中允许我们进行字符串比较,并且默认是不区分大小写的。上述代码中,如果构建的时候 NameAlice,那么 IsDefaultName 就是 true, 否则是 false

我们定义一个目标输出一下看看:

<Target Name="Print">
<Message Text="你好,$(Name),是否默认名字:$(IsDefaultName)" Importance="high" />
</Target>

运行:

msbuild build.proj

得到输出

你好,Alice,是否默认名字:true

而如果我们通过命令行传入一个 -property:Name=Bob,则输出就变成了:

你好,Bob,是否默认名字:false

另外,我们还可以使用 ChooseWhenOtherwise 来根据 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 产生警告;一些内置的任务(例如 DeleteCopy 等)也可能产生错误;对于自行编写的任务而言,也有其方式产生错误或者警告。

如果发生了错误,则构建默认会直接停止并以失败告终。但这不能满足所有需要,因此我们还可以在任务上利用 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 内置函数可以用,例如 ExistsHasMetadata 等等,具体可在 MSBuild 官方文档上查阅:

有了这些,我们便可以利用 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 的构建变得又快又省心。

重新认识 MSBuild - 1的更多相关文章

  1. Jenkins配置MSBuild实现自动部署(MSBuild+SVN/Subversion+FTP+BAT)

    所要用到的主要插件: [MSBuild Plugin] 具体操作: 1.配置MSBuild的版本 [系统管理]->[Global Tool Configuration]->[MSBuild ...

  2. 使用roslyn代替MSBuild完成解决方案编译

    原本我是使用批处理调用 MSBuild 完成解决方案编译的,新版的 MSBuild 在 Visual Studio 2015 会自带安装. 当然在Visual Studio 2015 中,MSBuil ...

  3. MSBuild 编译 C# Solution

    Microsoft(R) 生成引擎版本 4.6.1055.0 [Microsoft .NET Framework 版本 4.0.30319.42000] 版权所有 (C) Microsoft Corp ...

  4. UWP Jenkins + NuGet + MSBuild 手把手教你做自动UWP Build 和 App store包

    背景 项目上需要做UWP的自动安装包,在以前的公司接触的是TFS来做自动build. 公司要求用Jenkins来做,别笑话我,之前还真不晓得这个东西. 会的同学请看一下指出错误,不会的同学请先自行脑补 ...

  5. MSBuild的简单介绍与使用

    MSBuild 是 Microsoft 和 Visual Studio的生成系统.它不仅仅是一个构造工具,应该称之为拥有相当强大扩展能力的自动化平台.MSBuild平台的主要涉及到三部分:执行引擎.构 ...

  6. Jenkins学习九:Jenkins插件之构建MSBuild

    Jenkins是Java语言编写的,一直好奇是否可以构建NET语言的项目,目前只了解到有一个插件MSBuild支持构建NET项目. 一.Jenkins安装插件MSBuild 二.VS构建CsharpH ...

  7. CCNET+MSBuild+SVN实现每日构建

    最近开始将源代码迁移到SVN,于是便考虑到如何从SVN定期获取源码,自动编译并部署以减轻工作量并提高工作效率.通过多方搜集资料并进行研究,基本实现了这个功能.对于每日构建的概念就不具体展开了,可以在各 ...

  8. Msbuild项目集成右键菜单编译

    DS1.背景:   我们为什么要将VS2008命令行编译.sln文件集成到右键菜单呢? 原因一:VS2008很好很强大,但太费系统资源了,尤其是在虚拟机在里面装VS2008的时候更是如此. 原因二:有 ...

  9. 集成Visual Studio/MSBuild的开发/发布流程和 FIS3

    谁不想让自己的网站速度更快?为此需要多方面的优化,但优化又会增加开发工作量.Fis3 是很不错的前端优化工具,能够让前端的优化变得自动方便,解决前述问题.Fis3是百度开发的,开源的,在国内比较六流行 ...

  10. 用msbuild构建应用

    msbuild是微软提供的一个用于生成应用程序的平台,你可以通过一个xml配置文件来控制和处理你的软件工程.它也集成到了vs里面,它不依赖于vs. xml配置(架构)的组成元素: 项目文件 属性 项 ...

随机推荐

  1. Quantum CSS,一个超快的CSS引擎

    开始 本文翻译自Inside a super fast CSS engine: Quantum CSS,如果想要阅读原文,可以点击前往,以下内容夹杂本人一些思考,翻译也并不一定完全. 碎碎念 为什么翻 ...

  2. Vue的computed(计算属性)使用实例之TodoList

    最近倒腾了一会vue,有点迷惑其中methods与computed这两个属性的区别,所以试着写了TodoList这个demo,(好土掩面逃~); 1. methods methods类似react中组 ...

  3. web前端教程《每日一题》(1-99)完结

    第1期(2016年4月6日): (1)js中关闭当前窗口的方法是:window.close(); 第2期(2016年4月7日): (1)js中使字符串中的字符变为小写的方法是:toLowerCase方 ...

  4. 在微信小程序中绘制图表(part2)

    本期大纲 1.确定纵坐标的范围并绘制 2.根据真实数据绘制折线 相关阅读:在微信小程序中绘制图表(part1)在微信小程序中绘制图表(part3) 关注我的 github 项目 查看完整代码. 确定纵 ...

  5. 干货,看微信小程序后台用户数据如何演变和递增

    这几天发现附近小程序又多了好几家,其中有普通小程序和门店小程序,把它们做一个对比,门店小程序更多的像一张名片,只有基本的企业名称.地址.营业时间.电话和门店照片,和普通小程序相比显得逊色许多.楼下的水 ...

  6. 类其中的变量为final时的用法

    类其中的变量为final时的用法:   类当中final变量没有初始缺省值,必须在构造函数中赋值或直接当时赋值.否则报错. public class Test {     final int i;   ...

  7. Oracle中between 和 in

    select * from test_s where id between 2 and 12; between 就是左右全闭区间. SELECT columnsFROM tablesWHERE col ...

  8. 为Anaconda python3安装gi模块

    项目开发中需要使用到gi模块,Ubuntu自带的Python3.5可以正常使用gi.项目解释环境是Anaconda python3.5,提示ImportError: No module named ' ...

  9. python---概述

    python的主要应用领域 云计算:云计算的最火的语言,典型应用OpenStack. web开发:众多优秀的web框架,典型地有Django,众多大型网站也是python开发,比如YouTube.豆瓣 ...

  10. IETF 官网

    IETF 官网 https://www.ietf.org/ IETF数据追踪网站: https://datatracker.ietf.org/