MSBuild

创建可靠内部版本的最佳实践,第 2 部分

Sayed Ibrahim Hashimi

代码下载可从 MSDN 代码库
浏览代码联机

本文讨论:

  • 使用增量生成
  • 创建自定义任务
  • 管理生成引用
  • 生成大型数据树
本文涉及以下技术:
MSBuild,Visual Studio

内容

使用增量生成
创建自定义任务
管理生成参考
生成较大的源代码树

本文是讨论的我使用 MSBuild,生成托管的项目用于 Visual Studio 该引擎时,开发人员应遵循的最佳实践的第二部分。在第 1 部分中我将介绍一些基本操作和技术应用于大多数每个项目。此处在第 2 部分,我将介绍技术详细生成需要很大程度上因其大小的粗的自定义的配置。我将重点的主题使用增量生成创建自定义任务、 管理生成的引用以及构建大型数据树。

如果您不熟悉 MSBuild,一下我先前的 MSDN 文章:"用于创建可靠的最佳实践生成,第 1 部分", "内部 MSBuild: 编译的 Microsoft 生成引擎的应用程序您的方式自定义任务",和"WiX 技巧: 自动化使用 MSBuild 和 Windows Installer XML 版本."

使用增量生成

为生成变得更复杂,需要构建应用程序时间增加。生成一个大的产品可能需要几个小时。因为版本是耗时,应确保执行仅过期的目标。这一概念称为增量的构建在 MSBuild 支持通过输入的并输出目标元素的属性。MSBuild 遇到与输入和输出的目标时, 它将输出文件的时间戳,与输入文件的时间戳进行比较。如果输入的文件是最,目标执行 ; 否则,将跳过。

若要演示此机制,我将从部分"批处理任务"在本文的第 1 部分) 中的一组文件从复制一个位置到另一个使用示例。图 1 中的列表显示 Copy02.proj 文件,示例的修改版本。

图 1 Copy02.proj

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">

  <PropertyGroup>
    <SourceFolder>src\</SourceFolder>
    <DestFolder>dest\</DestFolder>
  </PropertyGroup>

  <ItemGroup>
    <!-- Get all files under src\ except svn files -->
    <SrcFiles Include="$(SourceFolder)**\*" Exclude="**\.svn\**\*"/>
  </ItemGroup>
  <Target Name="CopyToDest"
          Inputs="@(SrcFiles)"
          Outputs=
          "@(SrcFiles->'$(DestFolder)%(RecursiveDir)%(Filename)%(Extension)')">
    <Copy SourceFiles="@(SrcFiles)"
          DestinationFiles="@(SrcFiles->'$(DestFolder)%(RecursiveDir)%(Filename)%(Extension)')"
          />
  </Target>

  <Target Name="Clean">
    <ItemGroup>
      <_FilesToDelete Include="$(Dest)**\*"/>
    </ItemGroup>

    <Delete Files="@(_FilesToDelete)"/>
  </Target>
</Project>

此文件和 Copy01.proj 文件主要区别是使用输入和输出。 此处,CopyToDest 目标声明输入和作为输出目标文件复制这些文件。 如果复制的文件是最新,目标则跳过。 看看 图 2 中工作方式。

图 2 跳过 CopyToDest 目标

C:\Samples\Batching>msbuild Copy02.proj /fl /nologo
Build started 10/26/2008 6:15:37 PM.
Project "C:\Samples\Batching\Copy02.proj" on node 0 (default targets).
Skipping target "CopyToDest" because all output files are up-to-date with respect to the input files.
Done Building Project "C:\Samples\Batching\Copy02.proj" (default targets).

Build succeeded.
    0 Warning(s)
    0 Error(s)

在某些情况下,输出部分是最新但不是全部。 MSBuild 检测到这种情况下时, 输入的子集将被传递给要执行该目标中。

下面是一个示例。 在 Copy03.proj 文件具有相同的内容为 Copy02.proj,但有一个其他目标从输出目录中删除两个文件的 DeleteRandomOutputFiles。 :

<Target Name="DeleteRandomOutputFiles">
  <!-- Arbitrarily delete two files from dest folder  -->
  <ItemGroup>
    <_RandomFilesToDelete Include="$(DestFolder)class3.cs"/>
    <_RandomFilesToDelete Include="$(DestFolder)Admin\admin_class2.cs"/>
  </ItemGroup>

  <Delete Files="@(_RandomFilesToDelete)"/>   
</Target>

因为这两个文件将删除从目标,这样应该不跳过该目标,不再是最新,目标的输入。 如果执行命令的 msbuild Copy03.proj /t:DeleteRandomOutputFiles ; CopyToDest CopyToDest 后的已调用之后已经,您会看到 图 3 所示结果。

图 3 部分生成一个目标

C:\Samples\Batching>msbuild Copy03.proj /t:DeleteRandomOutputFiles;CopyToDest
Build started 10/26/2008 11:09:56 PM.
Project "C:\Samples\Batching\Copy03.proj" on node 0 (DeleteRandomOutputFiles;CopyToDest target(s)).
  Deleting file "dest\class3.cs".
  Deleting file "dest\Admin\admin_class2.cs".
CopyToDest:
Building target "CopyToDest" partially, because some output files are out of date with respect to their input files.
Copying file from "src\Admin\admin_class2.cs" to "dest\Admin\admin_class2.cs".
Copying file from "src\class3.cs" to "dest\class3.cs".
Done Building Project "C:\Samples\Batching\Copy03.proj" (DeleteRandomOutputFiles;CopyToDest target(s)).

Build succeeded.
    0 Warning(s)
    0 Error(s)

从该的输出可以看到部分生成 CopyToDest 目标,指其输入的部分列表已提供到目标。 目标仅必须复制而不是多两个文件。 为复杂的生成增量生成至关重要,并且您应始终创建支持此机制的目标。

创建自定义任务

当您编写自定义任务时,设计它是尽可能为可重复使用。 是例如 SendEmail 任务是比 SendEmailToJoe 任务更有用。 以下是您创建自定义任务时要遵循的一些指导。 我将展开整个此部分的每个指南。

  • 扩展任务或 ToolTask 类。
  • 适当地记录消息。
  • 必须以透明方式通信任务。
  • 为所有必需的输入参数,请使用所需的属性。
  • 使用项目文件可能感兴趣的所有属性上的输出属性。
  • 使用 ITaskItem 文件引用而不是字符串中。
  • 总是传送新输出值与输入相关的元数据。

时正在写入任务未必须直接实现 ITask (Microsoft.build.Framework) 接口。 更容易的方法是扩展任务或两者都 Microsoft.build.utilities 命名空间中找不到的 ToolTask 类。 如果要创建包装可执行文件的任务,您应使用 ToolTask 类。 对于其他情况下,使用任务类。

扩展这些类之一时, 您应使用日志属性帮助将邮件发送到附加的记录器。 不应依赖技术,例如 Console.WriteLine 将消息发送到控制台。 登录消息时, 很重要,这些记录在适当的级别,以便记录可以更有效地完成工作。 是例如应不记录事件为错误时实际上只是警告。 此外,如果将失败任务 ITask 接口的合同是返回 False 从 Execute 方法。 在这种情况下您应,以便用户知道如何解决的问题也始终记录至少一个错误。 如果 Execute 方法返回 true,应该记录错误。 这轻松地通过来实现使用一个 return 语句返回的! log.HasLoggedErrors。

在创建任务的另一个重要的原则是它们是独立。 任务需要了解有关生成的信息只是传递到其中的属性。 相反也是如此: 项目文件应只收集信息任务由其输出。 如果您设计与使用项目文件或记录通信的其他方法的任务,您紧密离合器它们,增加其的复杂性,并减少其可维护性。 始终,应采取一种"所看到您的获得"的方法构造您的代码只能查看任务的声明的输入和输出项目文件中的任务。

任务要求的输入的属性必须是用必需的属性 (位于 Microsoft.build.Framework 命名空间) 修饰的。 MSBuild 强制执行运行时的这一要求,并记录错误,指出它需要提供属性。 如果您的任务包含项目文件可能感兴趣的属性,应当将输出属性 (Microsoft.build.Framework) 放在这些属性。 因为输出参数必须显式修饰入带有输出属性的代码,您应错误在公开一些其他属性,因为这可能会阻止您不必重新部署任务只是以输出另一个属性的侧边。

如果任务可用来执行对文件或一组文件操作,这些文件应使用 ITaskItem 界面,而不只是字符串路径传递进出该任务。 例如,请考虑 图 4 所示,两个实现。

图 4 示例声明基于文件的属性

//This example shows how NOT to declare file based properties
public class Move1 : Task
{
    [Required]
    public string SourceFile
    { get; set; }

    [Required]
    [Output]
    public string DestinationFile
    { get; set; }

    public override bool Execute()
    {
        //TODO: Implementation here
        throw new NotImplementedException();
    }
}
//This example shows how to declare file based properties
public class Move2 : Task
{
    [Required]
    public ITaskItem SourceFile
    { get; set; }

    [Required]
    [Output]
    public ITaskItem DestinationFile
    { get; set; }

    public override bool Execute()
    {
        //TODO: Implementation here
        throw new NotImplementedException();
    }
}

Move1 类使用字符串传递出任务的一个文件。 Move2 而公开 ITaskItem 的实例。 传递字符串时, 所有您将是一个简单的字符串。 相反,ITaskItem 对象具有与其相关联元数据。

从一个使用者的角度实现是相同的 ; MSBuild 处理所需的任何转换。 此处显示的目标显示了如何这两个任务可以使用:

<Target Name="Example">

  <ItemGroup>
    <FileToMove Include="class1.cs"/>
  </ItemGroup>

  <Move1 SourceFile="@(FileToMove)" DestinationFile="dest\class1.cs" />
  <Move2 SourceFile="@(FileToMove)" DestinationFile="dest\class1.cs" />
</Target>

因此您可能会问为什么会也影响如果您的任务不使用元数据? 如果您开始编写的基于文件的引用中使用 ITaskItem 任务,任务会更加灵活。 此外,此简单的示例在这些任务接受单个文件一次。 那是最好创建可以移动多个文件的任务。 在这种情况下,应为 ITaskItem] 而不是 ITaskItem 定义属性。 这样您减少创建任务的多个实例的开销,您进行工作任务的用户更轻松。

最后,始终传输新的输出值与输入相关的元数据。 在种情况下您在此接受一些输入的项,然后创建新的输出与输入直接相关的从原始来源的所有元数据应转移到新创建的输出项,您不认识的甚至元数据中。 是例如如果您创建从 C++ 文件中创建对象文件的任务,Input 的属性为源的文件应声明为 ITaskItem],它因此应输出对象文件。 当您创建输出项时,每个对象文件然后需要相应的输入项分配给它的元数据。

原因在于重要这样添加到管道中每个后续步骤中项目的元数据。 是例如可以添加名为 DoNotLink 到 C++ 文件之一的元数据的一段。 编译器无法识别此元数据,它将通过它传递给在相应的对象文件项但然后链接目标将排除该对象文件。

管理生成参考

处理生成引用的最重要方面是永远不会引用驻留在全局程序集缓存 (GAC) 的程序集。 在 GAC 中专严格用于运行而不应包含了引用搜索路径中。 更好的方法是使用具有强名称的项目引用或程序集引用。

在创建程序集引用时可以使用一个 HintPath 来帮助确定从何处查找程序集的 MSBuild。 是例如的正确定义以下引用:

<Reference Include="IronMath, Version=1.1.0.0, Culture=neutral, 
                 PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
  <HintPath>..\contrib\IronPython-1.1\IronMath.dll</HintPath>
</Reference>

如果您发现设置的多个引用的 HintPath,下面是可能会帮助您的其他技巧: 通过添加您自己的目录名称扩展引用搜索路径。 包含用于在 C# 和 Visual Basic.NET 项目中引用解析的路径的列表项目为 AssemblySearchPaths 项目。 如果您将路径添加到此项目导入语句之后 Microsoft.CSharp.targets 或 Microsoft.VisualBasic.targets,该路径将第一个检查的位置。 可以实现这与属性声明如下:

<PropertyGroup>
  <AssemblySearchPaths>
    ..\..\MyReferences\;
    $(AssemblySearchPaths)
  </AssemblySearchPaths>
</PropertyGroup>

应该始终为 AssemblySearchPaths 和 HintPath 值使用相对路径。 如果指定使用完整的路径这些值,它将很难在其他计算机上生成该项目。

对于大型项目应避免将 CopyLocal 标志设置为 True 的引用。 文件标记要复制本地时, 引用该项目的每个项目将在本地的引用获取其副本的副本。 请考虑以下示例:

  • ClassLibray1 包含 10 CopyLocal 引用
  • ClassLibrary2 包含 5 个 CopyLocal 引用,并引用 ClassLibrary 1
  • WindowsFormApp1) 引用 ClassLibrary2

在这的种情况下 ClassLibrary 1 引用的所有将复制到输出文件夹 ClassLibrary 1、 ClassLibrary2,和 WindowsFormApp1。 所有 ClassLibrary2 引用将复制到输出文件夹 ClassLibrary2 和 WindowsFormApp1。 使您拥有 10 * 3 + 5 * 2 = 40 个文件复制某个完整的生成的并在本示例包含只有三个项目。 假设此问题影响包含 100 个或多个项目的生成。

要避免复制本地行为,可以将引用的专用元数据值设置为 False。 是例如,您可以修改对通过执行以下防止复制本地行为:

<Reference Include="IronMath, Version=1.1.0.0, Culture=neutral, 
               PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
  <HintPath>..\contrib\IronPython-1.1\IronMath.dll</HintPath>
  <Private>False</Private>
</Reference>

因为其他元数据引用上设置,将不能本地复制文件。 一个更好的方法是使用 ItemDefinitionGroup 元素定义默认的项目的元数据值。 在引用的情况下您可以设置为 false,默认情况下您的项目文件中的某处包括以下代码段专用的元数据。

<ItemDefinitionGroup>
  <Reference>
    <Private>False</Private>
  </Reference>
</ItemDefinitionGroup>

因为 Visual Studio 不会在默认情况下设置此元数据值,它将使用的不特别标记要本地复制的引用。

如果您有一个大型的生成,并且想以确保不会将文件复制本地,您可以覆盖 _CopyFilesMarkedCopyLocal 目标负责复制本地行为。 如果您导入语句之后覆盖此目标 Microsoft.CSharp.targets 或 Microsoft.VisualBasic.targets,您可以保证将本地复制任何文件。 通常建议不要重写任何元素包含一个前导的下划线,但这一种特殊情况。 本地引用复制会导致大量浪费的时间和驱动器空间在大的生成过程中以便重写该元素可以对齐。

生成较大的源代码树

处理大量的项目 (超过 100) 时, 需要组织您的项目,并具有有效的生成过程还灵活,以满足每个项目的需求。 本节中, 我将介绍用于组织源代码,以及用于将生成过程集成到该结构的方法的一个方法。 我描述的结构不适合每个团队或每个产品,但能够立即使用重要的方法为 modularize 在生成过程的如何以及如何引入所生成的所有产品的常见生成元素。

您可以将您的源组织到的最常见的项目在顶部之类的相关项目的树)。 此组织假定的项目需要生成的任何之下树中的项目和可能同级的项目,但它们不应直接生成上面这些节点中存在的项目。 是例如 图 5 显示了多个虚构的项目的依赖关系。

fig05.gif

图 5 项目依赖项 (单击图像可查看大视图)

此处我们有两个产品、 SCalculator 和 SNotepad 和它们所依赖的四个库。 我们可以将这些项目组织到树类似于 图 6 )。

图 6 SCalculator 和 SNotepad 组织树

ROOT
+---Common
    +---Common.IO
    ¦   ¦
    +---Common.UI
    ¦   ¦
    ¦   +---Common.UI.Editors
    ¦   
    +---Contrib
    ¦
    +---Products
    ¦   ¦
    ¦   +---SCalculator
    ¦   ¦   
    ¦   +---SNotepad
    ¦       
    +---Properties

因为所有项目都取决于常见的项目很 ROOT 节点正下方。 SCalculator 和 SNotepad 项目位于产品文件夹中。

您需要以下是使开发人员使用特定的子目录树生成所需代码段但不是一定整个结构的策略。 您可以通过一个约定,其中每个文件夹都包含三个 MSBuild 文件达到此:

  • NodeName.setting
  • NodeName.traversal.targets
  • dirs.proj

NodeName 是当前节点的名称例如,根、 公共或 common.ui.editors。 NodeName.setting 文件包含 (捕获为属性或项) 生成过程中使用的任何设置。 是例如此处包含的某些设置可能 BuildInParallel、 配置或平台。 NodeName.traversal.targets 文件包含用于生成项目的目标。 最后,dirs.proj 文件维护需要生成该子树的 ProjectFiles 项目) 中的项目的列表。

将 NodeName.setting 和 NodeName.traversal.targets 文件将始终导入,顶级的相应 files—root.setting 和 root.traversal.targets。 这些顶级的文件包含全局设置和目标,且节点级别文件插入自定义的位置。 在许多情况下这些节点级别文件需要导入只有根文件。

图 7 显示了 root.traversal.targets 文件的内容。 从根本上,三个目标文件中有此: 构建、 重建,和清理。 属性和其他目标是有只是为了支持这些三个目标。 该文件使用 ProjectFiles 项目的特定目录 dirs.proj 文件中声明。 dirs.proj 文件的要求是:

  1. 定义要使用 ProjectFiles 生成的所有项目。
  2. 导入朝着顶部 NodeName.setting 文件。
  3. 导入向底部 NodeName.targets 文件。

图 7 root.traversal.targets 文件

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">

  <!-- Targets used to build all the projects -->

  <PropertyGroup>
    <BuildDependsOn>
      $(BuildDependsOn);
      CoreBuild
    </BuildDependsOn>
  </PropertyGroup>

  <Target Name="Build" DependsOnTargets="$(BuildDependsOn)" />
  <Target Name="CoreBuild">
    <!--
    Properties BuildInParallel and SkipNonexistentProjects
    should be defined in the .setting file.
    -->
    <MSBuild Projects="@(ProjectFiles)"
             BuildInParallel="$(BuildInParallel)"
             SkipNonexistentProjects="$(SkipNonexistentProjects)"
             Targets="Build"
             />
  </Target>

  <PropertyGroup>
    <RebuildDependsOn>
      $(RebuildDependsOn);
      CoreRebuild
    </RebuildDependsOn>
  </PropertyGroup>
  <Target Name="Rebuild" DependsOnTargets="$(RebuildDependsOn)" />
  <Target Name="CoreRebuild">
    <MSBuild Projects="@(ProjectFiles)"
             BuildInParallel="$(BuildInParallel)"
             SkipNonexistentProjects="$(SkipNonexistentProjects)"
             Targets="Rebuild"
             />
  </Target>

  <PropertyGroup>
    <CleanDependsOn>
      $(CleanDependsOn);
      CoreClean
    </CleanDependsOn>
  </PropertyGroup>
  <Target Name="Clean" DependsOnTargets="$(CleanDependsOn)" />
  <Target Name="CoreClean">
    <MSBuild Projects="@(ProjectFiles)"
             BuildInParallel="$(BuildInParallel)"
             SkipNonexistentProjects="$(SkipNonexistentProjects)"
             Targets="Clean"
             />
  </Target>
</Project>

dirs.proj 文件应在该目录及其子目录中的所有项目中包括所有的项目。 它可以包含标准 MSBuild 项目 (如 C# 或 Visual Basic.NET 的项目或其他 dirs.proj 项目 (subdirectores)。 此文件不应在它上面的目录中包含存在的项目。 dirs.proj 文件应该假定已生成目录结构中更高的所需的项目。

如果生成对目录结构中较高的项目的项目引用的项目,并且该项目过期,它将生成自动。 因此,dirs.proj 文件不一定要指定要生成更高级别的项目。 此外的大容量的生成最好使用而不是项目引用的文件引用。 使用此方法中,如果切换到项目引用您不必修改您的生成过程只是您的引用。

下面是 root.setting 文件的内容:

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">
  <!--
  Global properties defined in this file
  -->
  <PropertyGroup>
    <BuildInParallel 
      Condition="'$(BuildInParallel)'==''">true</BuildInParallel>
    <SkipNonexistentProjects 
      Condition="'$(SkipNonexistentProjects)'==''">false</SkipNonexistentProjects>
  </PropertyGroup>

</Project>

此文件包含只有两个属性: BuildInParallel 和 SkipNonexistentProjects。 值得注意这些属性使用条件以确保不会覆盖任何以前存在的值,它允许轻松地自定义这些属性。 图 8 包含 ROOT 目录 dirs.proj 文件的内容。

图 8 内容 dirs.proj 文件的 ROOT Directory

<Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">

  <!-- Insert any customizations for settings here -->

  <Import Project="root.setting"/>

  <!-- Define all ProjectFiles here -->
  <ItemGroup>
    <ProjectFiles Include="Common\dirs.proj"/>
  </ItemGroup>

  <Import Project="root.traversal.targets"/>

  <!-- Insert any customizations for targets here -->

</Project>

此 dirs.proj 文件满足前面列出的所有三个条件。 如果 root.settng 文件中的值的任何自定义需要指定,会将它们放上面导入该文件,然后将该文件导入后放置目标的任何自定义。 此 dirs.proj 文件定义 ProjectFiles 项目只包含在 Common\dirs.proj 为文件负责构建其内容。 没有需要生成 ROOT 文件夹中的其他项目。 图 9 显示了 Common\dirs.proj 文件。

图 9 Common\dirs.proj 文件

 <Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003"
         ToolsVersion="3.5">

  <!-- Insert any customizations for settings here -->
  <PropertyGroup>
    <SkipNonexistentProjects>true</SkipNonexistentProjects>
  </PropertyGroup>


  <Import Project="common.setting"/>

  <!-- Define all ProjectFiles here -->
  <ItemGroup>
    <ProjectFiles Include="Common.csproj"/>
    <ProjectFiles Include="Common.IO\dirs.proj"/>
    <ProjectFiles Include="Common.UI\dirs.proj"/>
    <ProjectFiles Include="Products\dirs.proj"/>
  </ItemGroup>

  <Import Project="common.traversal.targets"/>

  <!-- Insert any customizations for targets here -->

  <PropertyGroup>
    < BuildDependsOn >
      CommonPrepareForBuild;
      $(BuildDepdnsOn);
      CommonBuildComplete;
    </BuildDependsOn>
  </PropertyGroup>

  <Target Name="CommonPrepareForBuild">
    <Message Text="CommonPrepareForBuild executed"
             Importance="high"/>
  </Target>
  <Target Name="CommonBuildComplete">
    <Message Text="CommonBuildComplete executed"
             Importance="high"/>
  </Target>
</Project>

此文件重写设置为 True 将 SkipNonexistentProjects 属性。 ProjetFiles 项目是用填充四个的值,并添加到生成依赖项列表中的这三个是 dirs.proj 文件中的几个目标。 如果您生成命令 msbuild.exe dirs.proj /t:build Common\dirs.proj 文件,您将看到生成所有项目和将执行自定义的目标。 我将不包括因空间限制的结果,但这些文件的源可以下载与本文的其他示例。

我们已经看几个关键建议使用可以更好地创建在本文中生成您的产品的过程。 与所有的最佳操作会有的这些规则可能无法应用时间 100 个百分比和需要有点弯曲的情况。 了解哪些这些做法适用于您在最佳方法是只为自己尝试每个。 我将 honored 有关如何在过您听到您文章好和差。

我想感谢 Dan Moseley 从 MSBuild 团队和 Brian Kretzler 有关这篇文章其宝贵帮助。

sayed Ibrahim Hashimi 佛罗里达大学具有了计算机工程程度。 他一直从事 MSBuild 由于早期的预览位,Visual Studio 2005 的发布。 他著 Inside the Microsoft Build Engine: Using MSBuild and Team Foundation Build (Microsoft Press 2009)。 他还是也是 Deploying .NET Applications: Learning MSBuild and Click Once (Apress,2006),并已写入包括 MSDN Magazine 的杂志的多个出版物。 他工作 Jacksonville,佛罗里达担任顾问和培训师,使用中,财务的专业技术培训和集合行业。 您可以访问 Sayed 在他的博客 sedodream.com.