团队基础生成自动化流程之最佳实践总论(III) – 重写生成号产生机制
微软真的偷懒了
在上一节讨论中已经提到, 我们希望每次生成所使用的生成号(BuildNumber)和附加在程序集上的版本标记一致.这样才能在程序集版本信息和特定的生成过程之间建立起联系. 本质上是管理的需求. 但是默认的生成号产生机制给我们带来了比较大的麻烦. 因为程序集版本号的格式, 一般是这样的: xx.xx.xxxxxx.xx, 即主版本号(Major Version No.), 次版本号(Minor Version No.), 生成号(Build No.), 修订号(Revision No.)
这是符合长期以来程序集的版本号命名格式习惯的, 也比较合理: 主版本号标示较大的功能更新或者说产品在特性(比如UI/主要业务逻辑更新/增加重大新特性)上有了翻天覆地的变化; 次版本号标示对主要功能进行修补, 主要定位是稳定性和兼容性问题; 生成号一般标示这是生成过程时间信息的, 由于通行的做法是这个号码每天加1, 所以现在几乎快要被理解成离某一天(比如项目开始那天)的天数了; 对应于生成号的实际含义, 修订号被用来标示当天的第几次生成过程. 这是比较通用的做法, 当然也有特例. 譬如腾讯QQ等版本号自恋狂软件(居然能搞出QQ 2009 beta3 sp2 这种版本号, 不过这个应该是对外发布的版本, 上面说的不是一回事儿, 权逗一乐).
但是, 微软默认提供的生成号完全不是这么回事. 我们在生成脚本上加上这样一段内容, 然后签入并运行, 看看TFS默认给我们生成的生成号:
- <Target Name="BuildNumberOverrideTarget">
- <Message Text="BuildNumber = $(BuildNumber)" />
- </Target>
大家可以看右图, 里面显示了默认的生成号. "OnDemand_20100325.1" - OnDemand是生成类型定义(BuildDefination), 20100325是当天的时间字符串, 1是修订版本(当天第几次生成). 这个是毫无疑问微软的同学们在偷懒了. 因为这个生成号很难直接用做程序集版本信息. 他们这么做, 唯一可以解释的是, 偷懒. 因为用这种方式的生成号是可以直接在版本号里面区分不同的生成类型定义的, 在生成过程结尾投递的时候, 可以直接在总的投递地址下面直接建一个以生成号为目录名的目录, 而不用建立对应的生成类型子目录.
重载版本号的流程问题
要想重写生成号, 我们需要首先搞清楚: 1)默认的生成号是什么时候产生的 2)哪里是最合适的重写位置. 上图是本系列第一篇文章里面介绍的TFS生成流程中的一部分, 是最开始的一部分. 那显然"BuildNumberOverrideTarget"就是专门提供给我们用来重写版本号的, 它之前的"InitializeBuildProperties"是用来产生版本号的, 它之后会调用"UpdateBuildnumberDropLocation"来更新生成程序集最后的投递地址. 下面这张图的高亮部分证明了InitializeBuildProperties是产生BuildNumber的来源:
现在我们知道BuildNumberOverrideTarget是为我们完成这个任务准备的最好的地方. 下面我们面临两个问题, 一个是技术性的: 如何编写自定义的生成号产生机制; 另外一个是实践性的, 在各种版本号生成机制中哪些方案比较好?
前一个技术性的问题, 不是本系列的范畴, 不过笔者推荐一篇浅显易懂的文章, <Custom Build Numbers in Team Build>. 有兴趣的可以看一下.
后一个问题是我们下一节讲述的重点.
重写生成号的选择
面对众多的生成号重写方案, 评价它的优劣, 其实只用两点标准就可以: 1. 看修订号是怎么生成的. 2.看是否依赖特定的环境(即必须符合代码仓库'自洽'的特性).
所谓修订号的生成,因为要保证它是单一叠加的, 所以必须知道上一次生成的修订号是多少. 所以很多重写方案采用的办法, 是将每次的生成号(或者修订号)写入到一个配置文件, 然后每次重写的时候取出这个修订号, 然后加一, 然后保存. 这样的做法是简单粗暴的. 如果这样一个文件保存在代码库中, 而我们给代码仓库配备了签入策略, 那么这个策略将每次都被破坏.
比较好的做法是:使用TFS默认产生的生成号中的修订号. 这样无需对本地文件或者代码库文件产生依赖.你还记得上一篇文章我们使用AssemblyInfo Task的时候有这样的属性吗?
- <AssemblyMajorVersion>4</AssemblyMajorVersion>
- <AssemblyMinorVersion>1</AssemblyMinorVersion>
- <AssemblyBuildNumber></AssemblyBuildNumber>
- <AssemblyRevision></AssemblyRevision>
完整方案是这样的: 创建自定义的任务, 任务接受的参数包括: TFS默认生成的生成号, AssemblyMajorVersion, AssemblyMinorVersion, AssemblyRevision. 任务内部计算今天离某一天的天数作为生成号. 任务内部分离出默认生成号的修订部分作为新生成号的修订部分. 然后组合输出新的BuildNumber,AssemblyRevision, AssemblyBuildNumber.
这样的好处, 即利用了TFS给我们提供的默认生成号避免我们自己搞一个配置文件, 又能有效的利用AssemblyInfo task的属性集. 而我们这个自定义任务的输出, 又可以给AssemblyInfo task提供BuildNumber, Revision等信息. 当我们准备迁移到新的版本时, 只要修改在生成脚本中的AssemblyMajorVersion和AssemblyMinorVersion就可以了. 如果您的代码仓库中多版本共存, 那么也可以保留这几个属性为空, 然后在build窗口的自定义属性中传入这些属性的值例如/p:AssemblyMajorVersion=4,AssemblyMinorVersion=3.
比较好的重写实现
现在你可以写这个自定义的Build Task了, 一个可能的实现可以是:
- using Microsoft.Build.Framework;
- using Microsoft.Build.Utilities;
- namespace JeffreySun.BuildNumberGenerater
- {
- public class GenerateBuildNumber : Task
- {
- private string originalBuildNumber = "DailyBuild_20100321.1";
- private string majorVersion = "1";
- private string minorVersion = "0";
- private string buildNumber = "00000";
- private string revision = "00";
- // Methods
- public override bool Execute()
- {
- try
- {
- // Split original build number and caculate with new build number (day count)
- return true;
- }
- catch (Exception ex)
- {
- base.Log.LogMessage("Task 'Generate Build Number' Failed! ");
- return false;
- }
- }
- // Properties
- [Required]
- public string OriginalBuildNumber
- {
- get
- {
- return this.originalBuildNumber;
- }
- set
- {
- this.originalBuildNumber = value;
- }
- }
- [Required]
- public string MajorVersion
- {
- get
- {
- return this.majorVersion;
- }
- set
- {
- this.majorVersion = value;
- }
- }
- [Required]
- public string MinorVersion
- {
- get
- {
- return this.minorVersion;
- }
- set
- {
- this.minorVersion = value;
- }
- }
- [Output]
- public string AssemblyBuildNumber
- {
- get
- {
- return this.buildNumber;
- }
- set
- {
- this.buildNumber = value;
- }
- }
- [Output]
- public string RevisionNumber
- {
- get
- {
- return this.revision;
- }
- set
- {
- this.revision = value;
- }
- }
- [Output]
- public string GeneratedBuildNumber
- {
- get
- {
- return (this.majorVersion + "." + this.minorVersion + "." + this.buildNumber + "." + this.revision);
- }
- }
- }
- }
然后在生成脚本中先引入后使用:
- <UsingTask TaskName="JeffreySun.BuildNumberGenerator.GenerateBuildNumber" AssemblyFile="$(Tasks)\JeffreySun.BuildNumberGenerator.dll" />
- <Target Name="BuildNumberOverrideTarget" Outputs="$(BuildNumber)">
- <GenerateBuildNumber OriginalBuildNumber="$(BuildNumber)" MajorVersion="$(AssemblyMajorVersion)" MinorVersion="$(AssemblyMinorVersion)" >
- <Output PropertyName="BuildNumber" TaskParameter="GeneratedBuildNumber" />
- <Output PropertyName="AssemblyBuildNumber" TaskParameter="AssemblyBuildNumber" />
- <Output PropertyName="AssemblyRevision" TaskParameter="RevisionNumber" />
- </GenerateBuildNumber>
- <Message Text="The new BuildNumber is : $(BuildNumber)" />
- </Target>
output的属性PropertyName是输出后被赋值的属性值, TaskParameter是在自定义task中使用的output参数名.
最后一点问题
这样, 我们解决了生成号和程序集版本信息的关联问题, 提出了重写生成号比较好的方案, 这个方案和AssemblyInfo能完美的集成. 事情到现在为止还比较美丽, 但是我们注意到, 我们还没解决代码仓库"自洽"的需求. 这个自定义版本号生成的dll, 我们还是倾向于放在代码仓库中(具体原因前面讲过了). 但是默认生成的流程顺序是: 重写版本号 -> 获取最新版本. 这就是悖论了, 我们需要这个dll来完成重写, 但是在没获取最新版本之前, 我们是没有这个dll的!
幸亏拜MSBuild超强的可扩展性和可定制化特性, 解决这个问题, 我们有乾坤挪移大法. 下节我们解释如何变动默认流程, 并顺便解释用这种办法, 解决本节的最后这点问题.
作者:Jeffrey Sun
出处:http://sun.cnblogs.com/
本文以“现状”提供且没有任何担保,同时也没有授予任何权利。本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。