终于到了不得不讨论这个话题的时候. 重写团队基础生成流程, 是团队基础最富于弹性和扩展能力的地方, 也是实践最多优劣各异的地方. 这是MSBuild引擎的优秀能力: 给MSBuild引擎提供任意一个格式正确的生成脚本, MSBuild引擎都能搞解析生成脚本并形成可以顺序(或并行)执行的执行顺序流. 所以我们现在回过头来看, Team Build是什么? 我们试着从本质上归结一下:
"在MSBuild引擎驱动下的, 以团队基础框架提供的包含一系列生成目标(Target)的默认生成脚本文件 - Microsoft.TeamFoundation.Build.targets - 为基础的, 可以被用户自定义生成脚本文件 - tfsbuild.proj - 所覆盖从而形成一条确定的可执行的生成流程."
既然这样, 我们就可以从战略上藐视Team Build流程了 : 只要我们理解了本系列第一篇文章所讲默认的生成流程, 理解某一目标定义在了什么地方(Where), 这一步在整个流程中的什么时间点上(When), 做了什么(What), 为什么这么做(Why), 那么如果我们觉得这个目标的相关4W中有一项或者几项让我们很不爽, 那么我们就完全可以改掉它! 不要担心你的做法是错误的, 只要修改是符合MSBuild生成脚本格式的, 是考虑了上下文的, 那么就尽管去做.
没错, 一个别扭的逻辑悖论让我们忍了很久了: "GET"的位置问题. 譬如上一篇我们提到了"Get"目标和我们自定义Task之间的时间逻辑问题: 我们要生成自己的版本号, 我们写了自己的Task来完成这个任务, 但是在重写版本号的时候, 我们的自定义任务居然还没到达本地位置!
Target这个概念是MSBuild的核心概念, 不单单用于Team Build, 包括我们平常在VS IDE里面点一下F6或者F5, 都是用到了MSBuild的这个概念(不信您随便找个项目文件用记事本打开看看第一行). 如果您对这个概念还有点不是很清楚的感觉的话, 我建议您一定看一下<MSDN Target Reference>, 然后再回来看这一节.
我们的目标是调整微软提供的默认生成流程. 修改的需求, 基本上可以归类为:
添加一个新的生成目标到合适的地方,
遮蔽一个已有的生成目标,
更改几个已有生成目标的执行顺序,
更改已有生成目标的执行条件或内容
修改, 需要修改的切口. 团队基础生成流程提供给我们的切口有两个: 需要修改的Target, 和它的DependsOnTargets属性.如果要修改一个生成目标的执行条件/对象或者执行的内容, 我们可以修改对应的Target本身; 如果调整几个Target的执行顺序, 那么我们可以修改它们的DependsOnTargets属性. 不管是修改Target本身还是它的DependsOnTargets属性, 我们修改的场所都是tfsbuild.proj文件. 值得强调的一点, MSbuild 只认识Target的名字(name属性), 或者DependsOnTargets属性指向的那个属性组的名字(PropertyGroup Name). 所谓重写, 就是相同名字的Target或者属性在tfsbuild.proj和Microsoft.TeamFoundation.Build.targets文件里的不同实现. MSbuild依赖于这些来构建生成流程. 如果您只想修改一个Target的执行条件, 那么最好不要修改这个Target的名字, 否则您得负责把这个改过名字的Target重新加入到生成流程的合适位置, 因为MSbuild不会记得它就是原来那个未修改的Target.
下面我们分类根据Team Build写几个具体的例子来讲述怎么完成重写.
向默认的生成流程中添加新的生成目标, 首先需要一个写好的Target, 可以写在TFSBuild.proj文件里, 也可以在TFSBuild.proj文件中使用import标记引入定义在别处的Target. 比如, 我们在使用AssemblyInfo Task的时候, 直接将一个现成的Target拷贝到TFSBuild.proj文件里:
其次我们需要确定在流程的什么位置加入这个Target - 其实本质上是找到合适的DependsOnTargets属性来扩充. 具体到AssemblyInfo的实例上, 我们想把这个target加入到clean之后, 编译之前, 那么我们就需要找到编译目标, 根据它的DependsOnTargets属性定位我们需要扩充的属性组来扩充它(MSBuild SideKick可以极大的帮助你找到合适的属性组). 然后我们发现, CoreCompileDependsOn是我们要扩充的属性组, 我们简单把我们的Target名字加入到其中的合适位置就可以了, 例如:
这里尤其需要注意的是依赖重叠问题, 比如你的生成目标依赖A目标, A目标依赖B目标, B目标最终又依赖你的生成目标. 所以在你添加新的生成目标的时候, 你需要确切的指导原来的前导和后继是什么, 确保你的生成目标加入后不会造成意外的流程改变.
遮蔽已有的生成目标相对简单. 在理解了怎么添加生成目标以后举一反三, 我们只要在TFSBuild.proj文件中添加修改后的对应的依赖属性组, 就可以达到遮蔽的目的:
例如, 默认的Team Build依赖属性如下:
可能您觉得您的服务器生成过程中不需要产生相应的文档(只是举例), 那么您就可以这样遮蔽它:
相当简单, 不是么? 不过您可能觉得默认的生成流程是Microsoft经过精心准备才提供的, 为什么要遮蔽它? 确实, 如果我们的动机仅仅是遮蔽, 那么确实没有什么生成目标确实是应该被遮蔽掉的, 毕竟每一个都有它的用处. 但是, 如果和调整生成目标的内容结合起来使用, 那么遮蔽就是一项很重要很有用的做法.
说到重点了. 我们其实最重大的一个问题, 就是GET的逻辑悖论问题. GET位置不当, 影响了很多自定义任务的使用. 这一节我们就拿调整GET相对于BuildNumberOverrideTarget的顺序来举例.
我们说生成目标之间的关系, 是由目标依赖属性确定的. 这种依赖包括两种方式, 即父子定义式: 几个被依赖的生成目标作为子目标共同构成作为依赖方的父生成目标; 兄弟顺序式: 同级的生成依赖属性以书写顺序构成执行顺序. 所以一个TFS生成流程, 在逻辑上首先是树状的. 父子关系通过生成目标依赖完成. 完整的TFS E2E Build的过程, 我们把它画了一张图, 你可以点击查看右图大图.
调整生成目标的顺序, 说到底还是调整几个生成目标的DependsOnTargets属性. 但是, 我们敏锐地注意到, 有些顺序的调整, 是跨父子关系的调整. 比如, GET是PreBuild的子目标, PreBuild是Team Build的子目标, Team Build和BuildNumberOverrideTarget又都是EndToEndIterationBuild的子目标.
现在进入实践环节: 究竟是把GET提前呢, 还是将BuildNumberOverride推后呢? 这时我们的原则是, 对原有其他生成目标影响最小. 那么显然是将BuildNumberOverride推后比较理想. 所以我们想要做的,调整BuildNumberOverrideTarget到Get和Label之间(因为Label要用到BuildNumber, 所以不能比Label更靠后). 了解了这些, 我们现在知道我们大概需要修改什么了. 我们首先找到EndToEndIterationDependesOn, 并遮蔽其中的BuildNumberOverrideTarget:
然后我们找到PreBuildDependsOn, 在其中添加BuildNumberOverrideTarget:
这样, 我们就成功地将GET和BuildNumberOverride的顺序调整完了. 额外的说一句, 调整顺序是一个需要耐心和精力的事情, 为了避免因改动过多过大造成修改失去控制, 所以实践的选择一般是以对其他生成目标造成的影响最小为佳. 另外也需要熟悉一些Target的具体做了什么, 避免因改动而造成一些Target需要的属性未被及时赋值. 这一方面, Reflector是个不错的工具.
更改已有Target的内容, 是这一篇文章最难的部分了. 我曾经花了一点精力考虑如何把这个问题讲得像喝白开水一样流畅, 未果. 于是我准备在下面简述这一修改的动机. 等到了这系列文章的第六篇 - <条件编译实践>中, 结合实例详细讲解这个问题.
修改已有Target内容的动机是什么? 这个问题我想我必须做出回答, 否则我们就不需要花这么多精力来做这件事情了. 修改生成目标的内容, 而不仅仅是它在生成过程中的位置, 这说明我们对它的内容不满. 那么TFS默认生成流程各个生成目标, 哪里最让我们不满? 答案里面至少应该包含条件编译. 即如果我们的项目是一个庞大的系统, 我们每次可能只想编译其中的某个模块A并生成安装包A, 这样的支持, 在对应的GET/CLEAN/COMPILE/TEST/PackageBinaries中, 并没有完全支持(部分生成目标支持指定目标范围). 所以我们可能要修改这些Target, 并让它乖乖的按照我们的设想运行.
那么在实践中, 究竟默认生成流程中的哪些部分需要或者经常被重写呢?
GET target在TFS默认生成流程中的位置, 是相当尴尬的. 我觉得可能是Microsoft的同学们一开始就没有认真的考虑, 或者在考虑之后选择了保守, 从而留下了GET和自定义任务之间的逻辑悖论问题. 这个问题的解决办法我们已经作为实例在前面讲解了. 判定自定义实践的优劣, 在这里可以简单归结为有否重写这个GET的位置. 见过各种各样稍微有些拙劣的实现, 比如先调用tf get命令, 或者copy命令等, 都不如这个重写优雅. 因为解决了本不应该这样实现的实现, 所以才有物归本源的优雅.
在.NET平台下, 我们的程序集, 以及我们的安装包, 都是应该先被数字签名后才能分发出去的. 我们知道在我们一般的开发过程中, 一个项目的属性的signing部分, 我们会像左图一样设置, 这就是为了在Build Automation的过程中给程序集签名做准备.
MSBuild默认提供了一个叫做SignFile的task, 可以用指定的key给指定的文件签名. 但是在具体实践中, 因为公司私钥的保密性要求非常高, 所以应该不是每个人都可以访问这个key文件. 有些公司的实践, 比如微软, 是将私钥放在一个安全性非常好的服务器上, 然后build automation过程将编译好的程序集提交到那个服务器的一个位置, 然后等待批准; 等批准通过, 再将文件取回. 这在各个不同公司采取的保密手段各不相同, 所以对应的CodeSign Target就会各不相同.
从位置选择上来讲, 对于程序集的CodeSign target最好放在PackageBinaries之前; 对于程序集的CodeSign target最好紧跟在PackageBinaries之后.
这里要说的就是条件编译了. 因为条件编译, 是面对越来越多的组建, 越来越复杂的系统的时候, 能够提高程序员工作效率的有效方式, 所以越来越成为必需的实践. 关于如何构建一个好的条件编译, 我们在本系列第六篇来讲.
最麻烦的一章结束了. 如果您认为这个系列的重头戏已经差不多了的话, 那您就误解了这个系列的含义了. 关于实践, 至于总论, 我们还有很多话要说.