团队基础生成自动化流程之最佳实践(VI) - 系统模块化条件编译

      软件生产和管理开始面临最严峻的考验. 随着需求的无限扩张, 企业级软件系统正变得越来越大. 出于管理需要, 可复用性要求, 和扩展性的需求, 根据软件工程学的实践, 我们开始将系统按照功能或按照部署切分,模块化. 于是整个系统的源代码的编译生成过程也就有了按模块切分的需求, 因为整个软件系统的生成可能会相当耗费时间. 譬如我们系统按照功能切分了A/B/C/D/E五个模块, 同时有1/2/3/4/5五个team分别负责对这五个功能模块进行开发. 假设ABCDE五个模块按照书写顺序反序编译, 每个模块编译需要的时间是5分钟. 如果我是1 team的一员, 我既不关心team build过程中其它模块的生成失败问题, 也不关心在本地生成的过程中其它模块的生成失败问题, 相反, 如果为了看到我的代码上的一点点修改能否通过编译就让我等25分钟, 而其中20分钟其实和我什么关系都没有, 我是会抓狂的. 所以我们会希望, team build 和desktop build能经过配置, 只生成我关心的部分以节约我的时间. 这个配置, 就是条件编译.

 

我们想要什么样的条件编译?


       软件系统的源代码在物理上总是以文件夹的形式组织的. 软件系统内部的功能模块划分, 体现在源代码组织上, 往往也是以文件夹的形势. 按照我们的一般理解, 功能模块以文件夹的父子级联形式由粗到细地从最顶级的功能划分一直细化到某个单一的C#项目(csproj, 仅以C#为例). 如何在逻辑上体现这种物理上的划分, 以致使Build的源代码范围可以使我们比较方便的定制, 是第一个重要的实践选择.
      笔者见过一些C++的软件项目的源代码逻辑组织, 是使用层级目录并在每一级目录下放置一个.inc或者功能类似的文件来指向下一级目录中有哪些目录(可能不是下一级的全部)包含应予编译的代码. 这既满足了物理上的划分, 也对源代码的逻辑关联做出了妥善的安排. 但是笔者极其反对直接将这样的做法直接移植到.NET的软件项目上! 因为语言和编译器的特性会直接影响生成过程, 而C++ 和 C#完全不同. C++需要的是恰当的头文件和makefile文件, 而C#要求的是恰当的sln文件和csproj文件.
      还有一种有相当迷惑性的做法。它和上面提到的方法类似, 但是利用了MSBuild的特性。它在每一级目录中提供一个msbuild兼容格式的proj文件, 指向下一级目录中的需要编译的csproj项目文件, 或者下一级proj文件(如果下一级还包含更下一级的目录)。 这种做法具有相当的迷惑性, 因为它比较好的管理好了源代码物理存放和逻辑上的关联, 又能比较好的控制Build的范围。 但是它对于developer没有带来任何的好处。 每个项目都如叶子般散落在各个目录中, developer在调试一些有其它项目依赖的项目时, 难于步进跟踪。developer更希望能按照自己的判断, 打开一个大小合适的solution进行工作(修改/调试/编译)。所以我们需要的是一个这样的方案, 它能满足:
      1.能满足源代码按照功能划分存放的物理要求
      2.能满足源代码按照逻辑组织在一起参与编译
      3.对MSbuild友好, 易于控制编译的范围
      4.对Developer友好, 能提供不同大小范围的源代码选择供developer使用

      按照这个方案进行比对, 我们总结如下:
      1. 使用层级目录存放功能划分开的代码;
      2. 代码按照功能级别, 由大到小, 由抽象到具体, 由粗粒度到细粒度存放;
      3. 在每一级(除了最低级目录)目录提供一个sln文件, 将本级的功能相关的项目添加到这个solution文件中
      4. 修改TFS默认的Team Build流程, 使其接受条件并根据条件全部或部分编译各个功能模块;
      5. 修改TFS默认的Desktop Build流程, 使其接受条件并根据条件全部或者部分编译各个功能模块;

SourceAndSolutionStructure      用一个简单的实例来说明。 假设我们正在编写一个机票销售和分析报表系统。 可以想象, 功能主要分3大块: 销售客户端, 机票销售web service, 以及分析报表模块。 机票销售web service又分为机票座次划分, 机票获取/修改, 机票预订等更细节的模块, 分析报表模块可能包含报表生成和智能趋势分析等更细节的模块。 对于这种情况, 按照我们所提的实践方案,用如左图所示的方法来实现源代码管理.

      左图中的绿色节点代表最基本的项目, 所有的棕色节点代表不同级别上设置的sln文件. 譬如我是client team的, 那么我每天只要打开OrderingClient.sln就完全可以正常工作了. 需要特别注意的是, 顶级的sln文件包含所有的项目. 另外我们这里仅仅是示意, 所以使用的例子不求多么复杂, 只为说明问题.

 

修改Team Build流程以适应模块化条件编译

      我们总揽本系列第一篇及第四篇提供的Team Build流程, 会对总体生成时间造成严重影响的, 是以下四个步骤:

        1. Get - 在条件编译中, 对不参加编译的其余模块源代码的GET操作.

        2. Compile - 在条件编译中, 我们正是要去除不参加编译的部分模块.

        3. Test - 在条件编译中, 对不参加编译的模块运行测试, 不但是浪费时间,而且会因为没有可运行的程序集而出错误.

        4. Packaging - 在条件编译中, 对不参加编译的模块生成的程序集进行打包, 不但是浪费时间, 而且会因为缺少被打包的对象而出错误.

        5. Drop - 在条件编译中, 不参加编译的模块没有生成对应的程序集, 所以Drop会出错.

      所以我们要修改这5个Target, 使其能满足模块条件编译的需求. 因为Compile, Test, Packaging是Team Build和Desktop Build共有的部分, 所以在这一段我们先来讲述如何调整Get和Drop.

调整GET

      实际上, TFS的Get target给我们的条件编译预留了这样的位置, 只是我们还没意识到. 如果您查看Microsoft.TeamFoundation.Build.targets文件, 你可以看到这样一段:

CoreGet
  1. <!-- The FileSpec to be used by the Get task.  When empty, all top-level folders in the workspace are used. -->
  2. <GetFileSpec Condition=" '$(GetFileSpec)'=='' "></GetFileSpec>
  3.  
  4. <Target Name="CoreGet"
  5.          Condition=" '$(SkipGet)'!='true' and '$(IsDesktopBuild)'!='true' "
  6.          DependsOnTargets="$(CoreGetDependsOn)" >
  7.   <!-- Get the sources for the given workspace-->
  8.   <Get TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
  9.         BuildUri="$(BuildUri)"
  10.         Workspace="$(WorkspaceName)"
  11.        Version="$(GetVersion)"
  12.        Filespec="$(GetFilespec)"
  13.        PopulateOutput="$(GetPopulateOutput)"
  14.        Overwrite="$(GetOverwrite)"
  15.        Preview="$(PreviewGet)"
  16.        Recursive="$(RecursiveGet)"
  17.        Force="$(ForceGet)">
  18.     <Output TaskParameter="Gets" ItemName="Gets" />
  19.     <Output TaskParameter="Replaces" ItemName="Replaces" />
  20.     <Output TaskParameter="Deletes" ItemName="Deletes" />
  21.     <Output TaskParameter="Warnings" ItemName="GetWarnings" />
  22.   </Get>
  23.   <SetBuildProperties Condition=" '$(GetVersion)' != '$(SourceGetVersion)' "
  24.                       TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
  25.                       BuildUri="$(BuildUri)"
  26.                       SourceGetVersion="$(GetVersion)" />
  27.   <PropertyGroup>
  28.     <SourceGetVersion>$(GetVersion)</SourceGetVersion>
  29.   </PropertyGroup>
  30. </Target>

所以我们要做的, 就是创建Property为GetFileSpec提供条件和值. 比如我们一般在tfsbuild.proj里面这样写:

ComponentToBuild
  1. <PropertyGroup>
  2.   <!--If not specified, compile the whole sources of this team project-->
  3.   <ComponentToBuild  Condition=" '$(ComponentToBuild)'=='' ">All</ComponentToBuild>
  4.   <GetFileSpec Condition=" '$(ComponentToBuild)'=='All' "></GetFileSpec>
  5.   <GetFileSpec Condition=" '$(ComponentToBuild)'=='ComponentA' ">specify server path containing component A here</GetFileSpec>
  6.   <GetFileSpec Condition=" '$(ComponentToBuild)'=='ComponentB' ">specify server path containing component B here</GetFileSpec>
  7. </PropertyGroup>

      这样, 当我们在引发Team Build的时候, 可以通过填写参数: /p:ComponentToBuild=ComponentA 来指定只编译A模块, 那么体现在Get操作上只获取A模块相关的源代码.

调整Drop

      Drop操作的修改就比Get简单多了, 因为Drop本质上就是拷贝, 而随着大家不同项目输出的不同, 这样的拷贝更是各种各样无法从一而论. 不过大体上我们只需要利用为Get创建的ComponentToBuild属性有条件的创建拷贝的一系列元组就能完成对Drop的修改了.

 

修改Desktop Build以适应模块化条件编译

      我们在这一小段中讲述如何调整Compile, Test和Packaging.

调整Compile

      在默认的tfsbuild.proj文件里, 有一组SolutionToBuild属性用Include指明了参加编译的solution. 谢天谢地, 我们前面提供的源代码存放与组织方案, 除了提供给developer方便之外, 刚刚好在这里极大的便利了条件编译, 我们只需要指定条件就能完成调整了:

SolutionToBuild
  1. <SolutionToBuild Include="$(SolutionRoot)\trunk\product\All.sln" Condition=" '$(ComponentToBuild)'=='All' "></SolutionToBuild>
  2. <SolutionToBuild Include="$(SolutionRoot)\trunk\product\ComponentAFolder\ComponentA.sln" Condition=" '$(ComponentToBuild)'=='ComponentA' "></SolutionToBuild>
  3. <SolutionToBuild Include="$(SolutionRoot)\trunk\product\ComponentBFolder\ComponentB.sln" Condition=" '$(ComponentToBuild)'=='ComponentB' "></SolutionToBuild>

调整Test

      与调整Compile完全类似, 此处不再赘述. 当然您可能需要花些实践合并那些vsmdi文件, 如果它们本来是分散的话.

调整Packaging

      一般来讲, 我们会使用InstallShield或者Wix来编写Packaging脚本. 在这里再一次推荐一下WiX. 另外使用VS Setup project的同学们请你们自己想办法吧, 因为怎么实践都会显得有些丑陋, 所以在这里就直接省略了. 那么我们假定使用了WiX来编写打包脚本. 编写打包脚本比较好的实践当然也是按照模块来编写, 例如本例中有了ComponentA.wxs和ComponentB.wxs. 那么我们的条件化打包就更直接了. 使用ComponentToBuild直接为WiX target指定执行条件即可.

 

现在我们站在了什么地方

恭喜, 你已经站在了条件编译实现了的地方.有两种方式来体验这种因省略不必要的耗费而带来的速度感:

1. 引发team build, 在最下面的框框里填写: /p:ComponentToBuild=ComponentA(这里当然换成您关心的那个模块名字)

2. 打开命令行, 输入msbuild tfsbuild.proj /p:ComponentToBuild=ComponentA

posted @ 2010-04-08 23:55  Jeffrey Sun  阅读(2059)  评论(4编辑  收藏  举报