版本信息是否值得在每次生成中更改
当创建一个.NET项目时, 系统会默认地在当前目录的Properties子目录中生成一个名为AssemblyInfo.cs的文件. 该文件包含了程序集各种相关信息比如版权, 公司, 文化, 版本等. 这些信息在程序集生成过程中会被用嵌入到程序集中, 并在将来显示在对应程序集文件的属性页上. 同时.NET的程序集定位机制也会用到其中的一些信息.
- // General Information about an assembly is controlled through the following
- // set of attributes. Change these attribute values to modify the information
- // associated with an assembly.
- [assembly: AssemblyTitle("AssemblyInfoDemo")]
- [assembly: AssemblyDescription("")]
- [assembly: AssemblyConfiguration("")]
- [assembly: AssemblyCompany("Sun")]
- [assembly: AssemblyProduct("AssemblyInfoDemo")]
- [assembly: AssemblyCopyright("Copyright © Sun 2010")]
- [assembly: AssemblyTrademark("Sun")]
- [assembly: AssemblyCulture("en-US")]
- // Setting ComVisible to false makes the types in this assembly not visible
- // to COM components. If you need to access a type in this assembly from
- // COM, set the ComVisible attribute to true on that type.
- [assembly: ComVisible(false)]
- // The following GUID is for the ID of the typelib if this project is exposed to COM
- [assembly: Guid("29044605-8c2e-4fed-8eee-723ccef7e435")]
- // Version information for an assembly consists of the following four values:
- //
- // Major Version
- // Minor Version
- // Build Number
- // Revision
- //
- // You can specify all the values or you can default the Build and Revision Numbers
- // by using the '*' as shown below:
- // [assembly: AssemblyVersion("1.0.*")]
- [assembly: AssemblyVersion("4.1.0.0")]
- [assembly: AssemblyFileVersion("4.1.0.0")]
从便于管理的角度来考虑, 我们希望程序集的版本信息和具体的生成版本号关联起来. 比如在集成测试中, 某些程序集可能会有点问题. 根据程序集的版本信息, 我们(或者一些自动化工具)可以确定生成该程序集的是哪次生成过程, 从而也能定位到对应的代码更改状态.更进一步的, 我们可以为此创建工作项来追踪这样的问题, 填写版本信息以方便工程师定位并解决问题.
但是, 如果我们在每次生成中都会修改这个版本信息, 会给程序集定位带来额外的负担. 举个具体的例子, app.config记录了某个程序集的版本信息, 那么我们在每次生成中都应该去修改这个信息为最新值, 以保证系统可以正常运行. 但是整体来说这个问题不是很大, 因为我们既可以在某些特定时期忽略程序集的版本信息(在app.config只使用程序集名称), 也可以使用自定义任务来完成这一功能修改.
所以衡量耗费和收益, 程序集版本信息是绝对值得随着每次生成而变更的.
如何更改版本信息
使用AssemblyInfo Task
团队基础的框架没有为我们提供默认的Task来完成这个任务. 不过现在这一实践的选择是相当多的(这也说明这一需求是多么旺盛). 其中比较完整也比较常见的是AssemblyInfo Task, 后来AssemblyInfo被包含到了MSBuild Extension Pack当中. AssemblyInfo task实际上给我们提供的是两个文件: Microsoft.VersionNumber.targets 和 AssemblyInfoTask.dll. 前者负责和TeamBuild生成脚本集成, 后者执行主要的逻辑. 没有接触过该任务的同学可以查看Aaron Hallberg写的博文: <Team Build and the AssemblyInfo Task>, 这里不予详细介绍. AssemblyInfo task的使用相当简单, 只要在TFSBuild.proj中加上Import语句并重写对应的属性就可以了(注意重写AssemblyInfoFiles属性, 默认的这个属性需要注意):
- <!--<Import Project="$(WixToolPath)Wix.targets"/>-->
- <Import Project="$(MSBuildExtensionsPath)\Microsoft\AssemblyInfoTask\Microsoft.VersionNumber.targets"
- Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\AssemblyInfoTask\Microsoft.VersionNumber.targets')" />
- <!-- Properties for controlling the Assembly Version -->
- <PropertyGroup>
- <AssemblyMajorVersion>4</AssemblyMajorVersion>
- <AssemblyMinorVersion>1</AssemblyMinorVersion>
- <AssemblyBuildNumber></AssemblyBuildNumber>
- <AssemblyRevision></AssemblyRevision>
- <AssemblyBuildNumberType>DateString</AssemblyBuildNumberType>
- <AssemblyBuildNumberFormat>yyMMdd</AssemblyBuildNumberFormat>
- <AssemblyRevisionType>AutoIncrement</AssemblyRevisionType>
- <AssemblyRevisionFormat>00</AssemblyRevisionFormat>
- </PropertyGroup>
- <!-- Properties for controlling the Assembly File Version -->
- <PropertyGroup>
- <AssemblyFileMajorVersion>4</AssemblyFileMajorVersion>
- <AssemblyFileMinorVersion>1</AssemblyFileMinorVersion>
- <AssemblyFileBuildNumber></AssemblyFileBuildNumber>
- <AssemblyFileRevision></AssemblyFileRevision>
- <AssemblyFileBuildNumberType>DateString</AssemblyFileBuildNumberType>
- <AssemblyFileBuildNumberFormat>yyMMdd</AssemblyFileBuildNumberFormat>
- <AssemblyFileRevisionType>AutoIncrement</AssemblyFileRevisionType>
- <AssemblyFileRevisionFormat>00</AssemblyFileRevisionFormat>
- </PropertyGroup>
现在我们开始面临实践的第一个选择, 从而引出衡量实践优劣的第一个重要原则. 这两个文件如何保存?
方案一: 使用提供的安装包, 将这两个文件安装到生成服务器上.
方案二: 将这两个文件直接签入到代码仓库中.
使用安装包的方案, 很明显这两个文件将存放在代码仓库之外的地方. 这样做的好处有: 所有团队项目都可以共享这个功能; AssemblyInfo task的使用不会受生成流程的限制(即随时可用); 缺点是, 所有需要使用这个功能的服务器或者个人工作台上都需要安装. 而将文件直接签入到代码仓库中, 则所有使用这一功能的地方都不必再安装, 但是相对应的, 该功能的使用受到生成流程的限制. "受到生成流程的限制"是什么意思? 是这样的, 任何一个第三方target文件, 我们都需要使用import显式地引入到当前脚本文件中. import标识的被引入文件, 在MSBuild解析生成脚本的初期就会被用到, 如果最终不能定位这个文件, 否则MSBuild会马上爆出"找不到文件"的错误从而退出. 但是生成过程的一般时间上的顺序是: 解析脚本 -> 获取最新版本. 这就是矛盾的地方. 如果没有"获取(Get)", 那本地就不会有这个文件, 那解析就会失败.
这个矛盾可以解决吗? 别着急, 先引出我们的
第一个重要原则: "EVERYTHING IN DEPOT"
代码仓库的设计初衷, 是保持独立性和完整性, 即它所持有的代码的生成过程不能依赖于其他环境特性尤其是生成服务器的特殊配置, 对于任意一个干净(对TFS来说, 仅安装操作系统和TF Build)的生成服务器, 代码仓库的代码都能够成功编译生成. "自洽"是代码仓库的内在驱动力.破坏了这一规则, 生成过程将难以控制和横向扩展, 同样也会严重影响代码仓库的其它使用者. 另外项目跟踪和控制的需求也隐含了这一原则.所以我们应该尽最大能力保持代码仓库的完整和独立, 除非有非常强烈的需求要求破坏这一原则.
根据这一原则我们毫无疑问会选择第二个实践方案. 那么如何解决上文提到的矛盾? 相当简单的做法也相当不错 - 将用到的Target直接拷贝到tfsbuild.proj文件里. 不过要注意不要把没用到Target也拷贝到tfsbuild.proj中, 那样只会污染这个文件. 估计有同学会骂我简单粗暴, 我倒是觉得这是一个度的问题. 因为Microsoft.VersionNumber.targets文件内容比较短, 只含有一个target. 把它拿到tfsbuild.proj来, 不但不会污染, 反而有利于提高可读性. 如果有同学说如果我需要import一个含有大量target的文件, 这时候怎么办, 我就一个问题: 真的需要引入这些第三方target吗? 答案大家可以自己考虑.
如何组织AssemblyInfo.cs文件
现在我们面临关于修改版本信息实践的第二个选择: 怎么组织这些AssemblyInfo.cs文件呢? 大体上也有两类主流做法:
放任自流, 代码仓库中有大量的AssemblyInfo.cs文件(大概是每个项目一个). 这样对开发人员是个喜讯, 因为根本不用为这个问题操心. Builder大概也不会很反对, 因为可以把责任丢给AssemblyInfo Task, 借助于AssemblyInfo Task的通配符, 由AssemblyInfo Task自己去搜索修改好了.
只在一个合适的地方放置一个AssemblyInfo.cs文件. 这个合适的地方可以是专门一个存放系统信息专用的目录, 比如存放版本信息/公钥等. 然后所有的项目都通过"添加已有项"->"作为链接添加"来链接到这唯一的一个文件上. 然后把这个文件指定给AssemblyInfo task就可以了, AssemblyInfo Task只修改这个文件, 所有链接了这个文件的项目都会获得更新后的版本信息, 无需AssemblyInfo task去搜索.
笔者推荐第二种方案. 为什么不用第一种方式? 一是效率问题, 二是AssemblyInfo task的通配不是很好用也不是很可靠, 三是修改多个文件比修改一个文件更容易出一些问题, 最后是分散管理容易带来不同步的问题(其实管理约束才是最重要的需求). 方案二是效率比较高的一种方式. 但是这里需要有一个文档(或者项目模板)定义来明确这种行为从而规避因为团队的后来者不清楚这一规定而带来的风险.