65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (42投票s)

2006年2月7日

12分钟阅读

viewsIcon

242802

downloadIcon

1054

MSBuild 技术入门。

作者 Patrick Smacchia
标题 实用 .NET2 和 C#2
出版社 Paradoxal Press
出版日期 2006年1月
ISBN 0-9766132-2-0
价格 33.99 美元
页数 896
网站 www.PracticalDOT.NET

Content

引言

.NET 2 平台附带了一个名为 msbuild.exe 的新工具。此工具用于构建 .NET 应用程序。它接受描述构建过程任务序列的 XML 文件,这与 makefile 文件精神一致。实际上,在项目初期,Microsoft 将此工具的代码命名为 XMakemsbuild.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.csFoo2.cs)以及一个名为 Image.jpg 的图像文件放在同一个文件夹中。

Foo2.cs

namespace Foo {
   public class Bar {
      public override string ToString() {
         return "Hi from Foo2";
      }
   }
}

Foo1.cs

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.netmoduleFoo1.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.exeFoo2.netmoduleImage.jpg 组成的程序集,我们必须执行以下两个命令:

>csc.exe /target:module Foo2.cs 
>csc.exe /Addmodule:Foo2.netmodule /LinkResource:Image.jpg  Foo1.cs

此外,我们希望在程序集构建完成后,这三个模块位于当前文件夹下的 \bin 子文件夹中。下面是完成相同工作的 MSBuild 项目 Example1.proj

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 文件夹的名称在我们的项目中使用了五次。这构成了一个很好的属性候选。

Example2.proj

<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 定义的预定义属性,例如:

属性

描述

MSBuildProjectDirectory

当前 MSBuild 项目所在的文件夹。

MSBuildProjetFile

当前 MSBuild 项目的名称。

MSBuildProjectExtension

调用 csc.exe。当前 MSBuild 项目的扩展名。

MSBuildProjectFullPath

当前 MSBuild 项目的完整路径。

MSBuildProjectName

当前 MSBuild 项目的名称(不含扩展名)。

MSBuildPath

包含 msbuild.exe 的文件夹。

在 Visual Studio 2005 中编辑属性时,您会注意到智能感知提供了一些键。例如,OutputPath 就是这样一个键。您可以使用这些键,但也可以定义自己的键。

Items

通过脚本构建项目是基于对文件夹、文件(源文件、资源文件、可执行文件……)以及引用(对程序集的引用、对 COM 类的引用、对资源文件的引用……)的操作。我们使用“项”(item)来指定这些条目,它们是大多数任务的输入和输出。在我们的示例中,Image.jpg 文件是 Copy 任务和第二个 Csc 任务都使用的项。Foo2.netmodule 文件是第一个 Csc 任务生成的项,并被第二个 Csc 任务使用。让我们使用项的概念重写我们的项目:

Example3.proj

<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 仅在文件夹不存在时执行。

Example4.proj

<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)')"/>
...

当定义了 OptimizeDebugSymbols 标准属性时,Csc 任务会自动考虑它们。

在启动此脚本之前,您必须在命令行参数中指定 Configuration 属性的值。这可以通过 /property(快捷方式 /p)选项完成:

>msbuild /p:Configuration=Release

敏锐的读者会注意到,可以使用条件来定义 Condition 属性的默认值。

Example5.proj

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup Condition="'$(Configuration)'==''">
      <Configuration>Debug</Configuration>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)'=='Debug'">
...

增量构建和目标之间的依赖关系

在实际环境中,MSBuild 项目的执行可能需要几分钟(甚至几小时)才能完成。(顺便说一句,您知道自 Windows 操作系统问世以来,它的构建时间大约需要 12 小时吗?这意味着不断增长的代码量会抵消机器性能的提升。)

对于对源文件所做的微小更改(而没有其他组件依赖于这些更改)而完全重新启动构建过程,这并不一定是好事。您还可以使用“增量构建”的概念。为此,您必须使用 InputsOutputs 属性为目标指定输入项和输出项的列表。如果 MSBuild 检测到至少一个输入项比一个输出项旧,它将执行该目标。

这种增量构建技术迫使您将任务划分为多个目标。我们已经看到,如果通过 MSBuild 指定多个目标(例如使用 DefaultTargets 属性),您不能假设任何执行顺序。但是,您可以使用 DependsOnTargets 属性指定目标之间的依赖关系集。MSBuild 仅在其依赖的所有目标都执行完成后才执行一个目标。自然,MSBuild 引擎会检测到目标之间的循环依赖关系并发出错误。

Example6.proj

<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.projExample8.target.proj,如下所示:

Example7.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>

Example8.target.proj

<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 的类似任务)。

Example9.cs

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\ 文件夹中)。

Example10.proj

<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 之类的工具来访问这些任务的代码。

© . All rights reserved.