实用 .NET2 和 C#2:MSBuild 入门






4.82/5 (42投票s)
2006年2月7日
12分钟阅读

242802

1054
MSBuild 技术入门。
|
Content
- 引言
- 不使用 MSBuild 构建多模块程序集
- .proj 文件、目标和任务
- 属性
- Items
- 条件
- 增量构建和目标之间的依赖关系
- MSBuild 转换
- 将 MSBuild 项目拆分到多个文件中
- Visual Studio 2005 如何利用 MSBuild?
- 创建自定义 MSBuild 任务
引言
.NET 2 平台附带了一个名为 msbuild.exe 的新工具。此工具用于构建 .NET 应用程序。它接受描述构建过程任务序列的 XML 文件,这与 makefile 文件精神一致。实际上,在项目初期,Microsoft 将此工具的代码命名为 XMake。msbuild.exe 可执行文件位于 .NET 安装文件夹 [Windows 安装文件夹]\Microsoft.NET\Framework\v2.0.50727\。计划将 MSBuild 集成到 Windows Vista 操作系统中。届时,它的作用范围将扩大,并可用于构建所有类型的应用程序。
到目前为止,要构建 .NET 应用程序,您需要
- 在 Visual Studio 中使用 Build 命令;
- 或者将 Visual Studio 的 devenv.exe 可执行文件用作命令行;
- 或者使用第三方工具,例如名为 NAnt 的开源工具,甚至使用调用 csc.exe C# 编译器的批处理文件。
MSBuild 旨在统一所有这些技术。熟悉 NAnt 的人不会感到陌生,因为 MSBuild 借鉴了该工具的许多概念。MSBuild 相对于 NAnt 的主要优势在于它被 Visual Studio 2005 使用。MSBuild 本身不依赖于 Visual Studio 2005,它是 .NET 2 平台的一部分。但是,Visual Studio 2005 用于构建项目的 .proj、.csproj、.vbproj 等文件是以 MSBuild XML 格式编写的。在编译过程中,Visual Studio 2005 使用 MSBuild 的服务。此外,MSBuild 使用的 XML 格式是完全支持和文档化的。对 MSBuild 的支持对 Visual Studio 来说是重要的进步,因为到目前为止,它使用的是未公开的构建脚本。
不使用 MSBuild 构建多模块程序集
在此,我们将创建一个包含以下内容的程序集:
- 一个主模块
Foo1.exe 。 - 一个模块 Foo2.netmodule。
- 一个资源文件 Image.jpg。
将 C# 源文件(Foo1.cs 和 Foo2.cs)以及一个名为 Image.jpg 的图像文件放在同一个文件夹中。
namespace Foo {
public class Bar {
public override string ToString() {
return "Hi from Foo2";
}
}
}
using System;
using System.Reflection;
[assembly: AssemblyCompany("ParadoxalPress")]
namespace Foo {
class Program {
public static void Main(string[] argv) {
Console.WriteLine("Hi from Foo1");
Bar b = new Bar();
Console.WriteLine( b );
}
}
}
由于我们希望构建一个包含多个模块的程序集,我们只能使用 csc.exe 命令行编译器,因为 Visual Studio 环境无法处理多模块程序集。
通过依次键入以下命令,创建 Foo2.netmodule 和 Foo1.exe 文件(csc.exe 编译器可以在 <WINDOWS-INSTALL-FOLDER>\Microsoft.NET\Framework\v2.0.50727 文件夹中找到)
> csc.exe /target:module Foo2.cs
> csc.exe /Addmodule:Foo2.netmodule /LinkResource:Image.jpg Foo1.cs
运行 Foo1.exe 可执行文件,程序将在控制台上显示以下内容:
Hi from Foo1
Hi from Foo2
.proj 文件、目标和任务
MSBuild XML 文档的根元素是 <Project>
。该元素包含 <Target>
元素。这些 <Target>
元素是名为“目标”的构建单元。MSBuild 项目可以包含多个目标,msbuild.exe 能够将多个目标的执行链接起来。当您通过命令行启动 msbuild.exe 时,它会将当前文件夹中的单个 .proj 文件作为输入。如果存在多个 .proj 文件,您必须指定要使用哪一个给 msbuild.exe。必须指定一个文件。
MSBuild 目标是一组 MSBuild 任务。<Target>
的每个子元素都构成了任务的定义。目标中的任务按声明顺序执行。MSBuild 提供了大约四十种类型的任务,例如:
任务类型 |
描述 |
复制 |
将文件从源文件夹复制到目标文件夹。 |
MakeDir |
创建文件夹。 |
Csc |
调用 csc.exe C# 编译器。 |
Exec |
执行系统命令。 |
AL |
调用 al.exe 工具(程序集链接器)。 |
ResGen |
调用 resgen.exe 工具(资源生成器)。 |
可能的任务的完整列表可在 MSDN 上的 MSBuild 任务参考 一文中找到。MSBuild 的一个有趣方面是,每种类型的任务都由一个 .NET 类实现。因此,可以通过提供自己的类来扩展 MSBuild 以支持新类型的任务。我们稍后将讨论这一点。
让我们回顾一下上一节中介绍的多模块程序集示例。请记住,要构建这个由三个模块 Foo1.exe、Foo2.netmodule 和 Image.jpg 组成的程序集,我们必须执行以下两个命令:
>csc.exe /target:module Foo2.cs
>csc.exe /Addmodule:Foo2.netmodule /LinkResource:Image.jpg Foo1.cs
此外,我们希望在程序集构建完成后,这三个模块位于当前文件夹下的 \bin 子文件夹中。下面是完成相同工作的 MSBuild 项目 Example1.proj:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="FooCompilation">
<MakeDir Directories= "bin"/>
<Copy SourceFiles="Image.jpg" DestinationFiles=".\bin\Image.jpg"/>
<Csc Sources="Foo2.cs" TargetType="module"
OutputAssembly=".\bin\Foo2.netmodule" />
<Csc Sources="Foo1.cs" TargetType="exe"
AddModules=".\bin\Foo2.netmodule" LinkResources="Image.jpg"
OutputAssembly=".\bin\Foo1.exe" />
</Target>
</Project>
我们看到名为 FooCompilation
的目标由四个任务构建:
- 一个
MakeDir
类型的任务,用于创建 \bin 文件夹; - 一个
Copy
类型的任务,用于将 Image.jpg 复制到 \bin 文件夹; - 两个
Csc
类型的任务,用于调用 csc.exe 编译器。
要执行此构建项目,您需要创建一个包含以下文件的文件夹:
.\Foo.proj
.\Foo1.cs
.\Foo2.cs
.\Image.jpg
使用命令行窗口(开始菜单 --> Microsoft .NET Framework SDK v2.0 --> SDK Command Prompt)进入该文件夹,然后运行 msbuild.exe 命令。每个目标都必须命名。默认情况下,msbuild.exe 只执行第一个目标。您可以使用 /target
(快捷方式 /t
)指定一个由分号分隔的目标列表。您也可以使用 <Project>
标签的 DefaultTarget
属性来指定这样的列表。如果指定了多个目标,则执行顺序是未定义的。
MSBuild 的默认行为是,一旦某个任务出现错误就停止执行。您可能希望构建脚本能够容忍错误。此外,每个包含任务的元素都可以包含一个 ContinueOnError
属性,该属性默认设置为 false
,但可以设置为 true
。
属性
为了让您能够使用参数来控制脚本,MSBuild 引入了属性的概念。属性是在 <PropertyGroup>
元素中定义的键/值对。MSBuild 属性作为别名系统工作。脚本中 $(key)
的每个出现都会被替换为关联的值。通常,/bin 文件夹的名称在我们的项目中使用了五次。这构成了一个很好的属性候选。
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<OutputPath>.\bin</OutputPath>
</PropertyGroup>
<Target Name="FooCompilation">
<MakeDir Directories= "$(OutputPath)"/>
<Copy SourceFiles="Image.jpg"
DestinationFiles="$(OutputPath)\Image.jpg"/>
<Csc Sources="Foo2.cs" TargetType="module"
OutputAssembly="$(OutputPath)\Foo2.netmodule" />
<Csc Sources="Foo1.cs" TargetType="exe"
AddModules="$(OutputPath)\Foo2.netmodule"
LinkResources="Image.jpg"
OutputAssembly="$(OutputPath)\Foo1.exe" />
</Target>
</Project>
您还可以使用 MSBuild 定义的预定义属性,例如:
属性 |
描述 |
|
当前 MSBuild 项目所在的文件夹。 |
|
当前 MSBuild 项目的名称。 |
|
调用 csc.exe。当前 MSBuild 项目的扩展名。 |
|
当前 MSBuild 项目的完整路径。 |
|
当前 MSBuild 项目的名称(不含扩展名)。 |
|
包含 msbuild.exe 的文件夹。 |
在 Visual Studio 2005 中编辑属性时,您会注意到智能感知提供了一些键。例如,OutputPath
就是这样一个键。您可以使用这些键,但也可以定义自己的键。
Items
通过脚本构建项目是基于对文件夹、文件(源文件、资源文件、可执行文件……)以及引用(对程序集的引用、对 COM 类的引用、对资源文件的引用……)的操作。我们使用“项”(item)来指定这些条目,它们是大多数任务的输入和输出。在我们的示例中,Image.jpg 文件是 Copy
任务和第二个 Csc
任务都使用的项。Foo2.netmodule 文件是第一个 Csc
任务生成的项,并被第二个 Csc
任务使用。让我们使用项的概念重写我们的项目:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup><OutputPath>.\bin</OutputPath></PropertyGroup>
<ItemGroup>
<File_Image Include="$(OutputPath)\Image.jpg"/>
<NetModule_Foo2 Include="$(OutputPath)\Foo2.netmodule"/>
</ItemGroup>
<Target Name="FooCompilation">
<MakeDir Directories= "$(OutputPath)"/>
<Copy SourceFiles="Image.jpg"
DestinationFiles="@(File_Image)"/>
<Csc Sources="Foo2.cs" TargetType="module"
OutputAssembly="@(NetModule_Foo2)" />
<Csc Sources="Foo1.cs" TargetType="exe"
AddModules="@(NetModule_Foo2)"
LinkResources="@(File_Image)"
OutputAssembly="$(OutputPath)\Foo1.exe" />
</Target>
</Project>
我们注意到使用 @(item name)
来引用项。此外,项可以使用通配符语法定义一组文件。例如,以下项引用当前文件夹中除 Foo1.cs 之外的所有 C# 文件:
<cs_source Include=".\*.cs" Exclude=".\Foo1.cs" />
条件
我们可能希望同一个 MSBuild 项目构建多个不同的版本。例如,为了处理同一个应用程序的 Debug 和 Release 版本的构建而创建和维护两个项目会很可惜。此外,MSBuild 引入了条件的概念。可以将 Condition
属性添加到 MSBuild 项目的任何元素(属性、项、目标、任务、属性组、项组……)。如果在执行过程中,某个元素的条件不满足,MSBuild 引擎将忽略它。MSDN 文章 MSBuild Conditions 描述了可在 Condition
属性中使用的表达式的完整列表。
在以下示例中,我们使用“字符串相等比较”类型的条件来确保我们的脚本同时支持 Debug 和 Release 模式。我们还使用“文件或文件夹存在性检查”类型的条件来仅在需要时执行 MakeDir
任务。此条件纯粹是为了学习目的而存在,因为 MakeDir
仅在文件夹不存在时执行。
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<Optimize>false</Optimize>
<DebugSymbols>true</DebugSymbols>
<OutputPath>.\bin\Debug</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<Optimize>true</Optimize>
<DebugSymbols>false</DebugSymbols>
<OutputPath>.\bin\Release</OutputPath>
</PropertyGroup>
<ItemGroup>
...
<Target Name="FooCompilation">
<MakeDir Directories= "$(OutputPath)"
Condition="!Exists('$(OutputPath)')"/>
...
当定义了 Optimize
和 DebugSymbols
标准属性时,Csc
任务会自动考虑它们。
在启动此脚本之前,您必须在命令行参数中指定 Configuration
属性的值。这可以通过 /property
(快捷方式 /p
)选项完成:
>msbuild /p:Configuration=Release
敏锐的读者会注意到,可以使用条件来定义 Condition
属性的默认值。
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)'==''">
<Configuration>Debug</Configuration>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
...
增量构建和目标之间的依赖关系
在实际环境中,MSBuild 项目的执行可能需要几分钟(甚至几小时)才能完成。(顺便说一句,您知道自 Windows 操作系统问世以来,它的构建时间大约需要 12 小时吗?这意味着不断增长的代码量会抵消机器性能的提升。)
对于对源文件所做的微小更改(而没有其他组件依赖于这些更改)而完全重新启动构建过程,这并不一定是好事。您还可以使用“增量构建”的概念。为此,您必须使用 Inputs
和 Outputs
属性为目标指定输入项和输出项的列表。如果 MSBuild 检测到至少一个输入项比一个输出项旧,它将执行该目标。
这种增量构建技术迫使您将任务划分为多个目标。我们已经看到,如果通过 MSBuild 指定多个目标(例如使用 DefaultTargets
属性),您不能假设任何执行顺序。但是,您可以使用 DependsOnTargets
属性指定目标之间的依赖关系集。MSBuild 仅在其依赖的所有目标都执行完成后才执行一个目标。自然,MSBuild 引擎会检测到目标之间的循环依赖关系并发出错误。
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTargets="FooCompilation">
...
<Target Name="CreateOutputPath" Condition="!Exists('$(OutputPath)')">
<MakeDir Directories= "$(OutputPath)"/>
</Target>
<Target Name="FooCompilation" DependsOnTargets="CreateOutputPath"
Inputs="Foo2.cs;Foo1.cs"
Outputs="@(NetModule_Foo2);$(OutputPath)\Foo1.exe">
...
</Target>
</Project>
MSBuild 转换
您有机会在目标的输入项和输出项之间建立一对一的对应关系。为此,您需要使用 MSBuild 转换,这些转换在 MSDN 上的 MSBuild Transforms 一文中进行了详细介绍。使用转换的优点是,如果 MSBuild 检测到至少一个输入项比与其对应的输出项旧,它将执行目标。从逻辑上讲,这样的目标将不那么频繁地执行,从而带来性能提升。
将 MSBuild 项目拆分到多个文件中
我们已经看到 msbuild.exe 工具每次执行只能处理一个项目文件。但是,MSBuild 项目文件可以使用 <Import>
元素导入另一个 MSBuild 项目文件。在这种情况下,导入文件中 <Project>
的所有子元素都将被复制到 <Import>
元素的位置。然后,我们的示例脚本可以拆分为两个文件 Example7.proj 和 Example8.target.proj,如下所示:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTargets="FooCompilation">
<PropertyGroup Condition="'$(Configuration)'==''"> ...
<PropertyGroup Condition="'$(Configuration)'=='Debug'"> ...
<PropertyGroup Condition="'$(Configuration)'=='Release'"> ...
<ItemGroup> ...
<Import Project="Foo.target.proj"/>
</Project>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
<Target Name="CreateOutputPath" ...
<Target Name="FooCompilation" ...
</Project>
Visual Studio 2005 如何利用 MSBuild?
我之前提到过,Visual Studio 2005 用于构建项目的 .proj、.csproj、.vbproj 等扩展名的文件是以 MSBuild XML 格式编写的。如果您分析这样的文件,您会注意到没有明确指定目标。实际上,Visual Studio 2005 生成的项目文件会导入一些包含通用目标的 .targets 文件。例如,一个扩展名为 .csproj 的文件包含以下元素:
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
这个 Microsoft.CSharp.targets 文件包含两个通用目标:
- 一个名为
CreateManifestResourceNames
的目标,它负责组织资源文件(将 .resx 文件转换为 .resources)。 - 一个名为
CoreCompile
的目标,其中包含一个名为Csc
的任务,负责构建 C# 文件。
我们建议您查看位于 $(MSBuildBinPath)
定义的文件夹中的 .targets 文件,该文件夹是 .NET 2.0 安装文件夹(即 [Windows 文件夹]\Microsoft.NET\Framework\v2.0.50727)。
除了导入这样的 .targets 文件之外,Visual Studio 2005 生成的文件主要包含将由通用目标使用的属性和项的定义。
创建自定义 MSBuild 任务
MSBuild 的另一个有趣方面是,每种类型的任务都体现在一个 .NET 类中。这意味着可以通过提供自己的类来扩展 MSBuild 以支持新任务类型。这样的类必须支持以下约束:
- 它必须实现 Microsoft.Build.Framework.dll 中定义的
ITask
接口,或者派生自 Microsoft.Build.Utilities.dll 中定义的辅助类Task
。 - 它必须实现
ITask
接口的bool Execute()
方法。此方法包含任务的主体,并且如果执行成功,必须返回true
。 - 它可以提供属性,这些属性将在调用
Execute()
之前由 MSBuild 从任务中的属性值设置。只有标记为Required
的属性才必须初始化。
这是一个名为 MyTouch
的任务的示例,该任务会更新 Files
属性指定的文件的时戳(值得一提的是,MSBuild 框架提供了名为 Touch
的类似任务)。
using System;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace MyTask {
public class MyTouch : Task {
public override bool Execute() {
DateTime now = DateTime.Now;
Log.LogMessage(now.ToString() +
" is now the new date for the following files:");
try {
foreach(string fileName in m_FilesNames) {
Log.LogMessage(" " + fileName);
System.IO.File.SetLastWriteTime(fileName, now);
}
}
catch (Exception ex) {
Log.LogErrorFromException(ex, true);
return false;
}
return true;
}
[Required]
public string[] Files {
get { return (m_FilesNames); } set { m_FilesNames = value; }
}
private string[] m_FilesNames;
}
}
请注意 TaskLoggingHelper Task.Log{get;}
的使用,它允许显示与任务执行相关的信息。我们的 MyTouch
任务必须注册到所有可能使用它的 MSBuild 项目中。这是通过使用 <UsingTask>
元素完成的。这是一个更新当前文件夹中 C# 文件时戳的脚本(MyTask.dll 程序集必须位于 C:\CustomTasks\ 文件夹中)。
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask AssemblyFile="C:\CustomTasks\MyTask.dll"
TaskName="MyTask.MyTouch"/>
<ItemGroup>
<FichierSrcCs Include="*.cs"/>
</ItemGroup>
<Target Name="TouchTheCsFiles" >
<MyTouch Files= "@(CsSrcFiles)"/>
</Target>
</Project>
有趣的是,所有标准任务都通过 Microsoft.Common.Tasks 文件中的 <UsingTask>
元素进行声明。该文件在每次执行时由 msbuild.exe 自动隐式导入。通过分析此文件,我们可以看到对应于标准任务的类定义在 Microsoft.Build.Tasks.dll 程序集中。然后,您可以使用诸如 Reflector 之类的工具来访问这些任务的代码。