CCNET+MSBuild+SVN实时构建的优化总结
本文不是介绍如何使用CCNET+MSBuild+SVN构建自动编译系统,相关的内容可以从很多地方获取,可以再园子里搜一下。
随着我们的SVN库日益壮大,容量达到10G,几十G 甚至更大时,我们发现自动构建速度越来越慢,直到有一天你发现入了很小一段代码却不得不等待几小时构建完成,程序员的忍受是有极限的,因此我们决定采取措施实施优化。
首先,我们必须分析哪些因素导致了我们构建速度的减慢,罗列一下,大概如下几个方面:
1. SVN库太大,使得构建服务器在更新SVN代码时花费大量时间。
2. SVN库里有很多工程,每当有SVN代码更新的时候,CCNET就会调用MSBuild将我们所有的工程都编译一遍。(即使入库的文件根本不需要编译,如python脚本)
3. SVN库中工程量越来越大,导致编译所有工程时间原来越长。
对于第三点,我们没有办法,但对于前两点,我们是有办法解决的,总结一下要做的事情:一是加快SVN更新速度,二是减少不必要的工程编译次数。
一、加快SVN更新速度
SVN的更新操作是有CCNET发起的,服务每隔一段时间查询一次SVN是否更新(看CCNET源码好像是调用svn --log来获取代码更新信息),如果有文件更新,则调用svn --update进行更新。从CCNET源码看来,CCNET对SVN代码的更新应该是针对性的,即,查询到哪部分代码有更新,就只更新那部分代码。这样的话效率应该不差。但在实际过程中,发现CCNET调用SVN更新速度异常的慢,甚至让我怀疑它是对整个SVN库执行了一次update操作。
要加快SVN更新速度,我们想到的是减少SVN更新的文件范围,假如你入库了一个python代码,或是QTP测试案例,因为无需编译,所以构建服务器甚至不需要更新那部分代码。因此,我们可以在CCNET的配置文件中只配置我们需要编译的工程:
<sourcecontrol type="multi"> <sourceControls> <svn> <trunkUrl>http://xxx/projectA</trunkUrl> <workingDirectory>x:\ccnet\svn\projctA</workingDirectory> <username>name</username> <password>pwd</password> <executable>x:\ccnet\Subversion\svn.exe</executable> </svn> <svn> <trunkUrl>http://xxx/projectB</trunkUrl> <workingDirectory>x:\ccnet\svn\projctB</workingDirectory> <username>name</username> <password>pwd</password> <executable>x:\ccnet\Subversion\svn.exe</executable> </svn> <svn> <trunkUrl>http://xxx/projectC</trunkUrl> <workingDirectory>x:\ccnet\svn\projctC</workingDirectory> <username>name</username> <password>pwd</password> <executable>x:\ccnet\Subversion\svn.exe</executable> </svn> </sourceControls> </sourcecontrol>
通过上面的设置,CCNET就是监视我们上面指定的SVN路径的代码更新了,如果你的SVN库中有大量不需要编译的文件,这样的优化带来的效果是巨大的。
二、减少编译次数
上面解决了对入库不需要编译的代码文件的问题,但我们还需要面临一个问题是,当你入库工程A的代码时,你只希望编译工程A,而不是将工程A,B,C都编译一遍。甚至,可能还有更加严格的要求。比如,我们库中有个公共库的工程FrameworkA,工程ProjectA,ProjectB,ProjectC都使用到了该公共库工程。我们希望做到:
1. 当我入库的代码属于FrameworkA时,希望把ProjectA,ProjectB,ProjectC都编译一遍。(因为我修改了公共库,很有可能导致工程A,B,C编译不过。)
2. 当我入库的是ProjectA(或B,C)时,我只希望编译ProjectA(或B,C)就行了。
我们看到我们的工程之间多了一些内在的联系,如何才能处理这种复杂的编译关系呢?我想到的是,要么在CCNET上做手脚,要么在MSBuild上进行扩展。CCNET是一个开源项目,我完全可以修改它的代码为我所用,甚至修改出一个更适合使用的版本提交上去 ,但发现这样做的工程量太大,需要花费的精力太多。我需要找到一个简单的,又容易实现的方案,达到我们上面的两点需求。因此,我选择了对MSBuild进行扩展,而MSBuild本事又是支持这种扩展的,这给我带来了很大的方便。
熟悉MSBuild配置文件的朋友一定知道里面有很多Task供我们使用,比如:CallTarget,Exec,MakeDir,VCBuild等等。同时,也提供机制让我们实现自己的自定义Task。详细使用可以参考微软的文档:How to write a Task
现在,我们可以实现一个自己的Task了,那么在我们自定义的这个Task里,我们应该做些什么呢?恩,再来整理一下思路:
1. 我们需要知道更新的代码属于哪个工程。
2. 我们需要知道编译该工程的同时,还需要编译哪些与之相关的工程。
首先解决第一个问题,如何知道更新的代码属于哪个工程?其实,一个更加实际的问题,如何知道更新了哪些代码? 我曾经尝试过使用CCNET一样的办法,调用svn --log对入库记录进行查询,然后每次保存好上次更新的状态,再判断这次更新相对于上次改动了哪些。做到这些其实非常容易,但是,存在一个问题,CCNET本身也有一个机制在记录着SVN更新的状态(state文件),如果我又记录一个自己的SVN更新历史的文件,可能和CCNET本身记录的有时间差,使得整个流程下来对于要更新的和编译的代码文件变得非常不确定。因此,我最后打算直接使用CCNET获取到的文件更新列表。要获取CCNET获取的SVN更新列表,只需要在CCNET的配置文件中加入下面一段:
<prebuild> <modificationWriter> <filename>mods.xml</filename> <path>x:\ccnet\svn\build</path> </modificationWriter> </prebuild>
这样,每当CCNET更新SVN代码时,都会将SVN的更新记录到mods.xml中,mods.xml的格式大致如下:
<?xml version="1.0" encoding="utf-8"?> <ArrayOfModification xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Modification> <Type>Modified</Type> <FileName>xxx.cs</FileName> <FolderName>/trunk/ProjectA/</FolderName> <ModifiedTime>2009-04-05T16:09:58.545196+08:00</ModifiedTime> <UserName>coderzh</UserName> <ChangeNumber>8888</ChangeNumber> <Version /> <Comment>Upload My Greate Code</Comment> </Modification> </ArrayOfModification>
回到正题,通过读取mods.xml知道CCNET此次编译前更新的代码后,如何判断改代码文件属于哪个工程呢?很容易想到的就是通过路径判断,比如上面的代码的FolderName是/trunk/ProjectA,我们就能断定该代码文件属于ProjectA。当然,我们还需要一个配置文件,用于说明哪些目录下的代码属于哪个工程,即代码文件与工程的对应关系。这些信息我们可以直接在MSBuild的配置文件中设置:
<PropertyGroup> <FrameworkAPath>\trunk\Framework</FrameworkAPath> <ProjectA>\trunk\ProjectA</ProjectA> <ProjectB>\trunk\ProjectB</ProjectB> <ProjectC>\trunk\ProjectC</ProjectC> </PropertyGroup> <ItemGroup> <SvnFolder Include="$(FrameworkAPath);"> <ProjectName>FrameworkA</ProjectName> </SvnFolder> <SvnFolder Include="$(ProjectAPath);"> <ProjectName>ProjectA</ProjectName> </SvnFolder> <SvnFolder Include="$(ProjectBPath);"> <ProjectName>ProjectB</ProjectName> </SvnFolder> <SvnFolder Include="$(ProjectCPath"> <ProjectName>ProjectC</ProjectName> </SvnFolder> </ItemGroup>
OK,我们的第一个问题解决了,接下来的问题是,如何设置工程间的这种关联关系。同样的,我们通过MSBuild配置文件中的Target来设置,我们看下面的配置就会明白了:
<Target Name="FrameworkA"> <MSBuild Projects="$(FrameworkAPath)\FrameworkA.sln" Properties="Configuration=Release"/> <CallTarget Targets="ProjectA" /> <CallTarget Targets="ProjectB" /> <CallTarget Targets="ProjectC" /> </Target> <Target Name="ProjectA"> <MSBuild Projects="$(ProjectAPath)\ProjectA.sln" Properties="Configuration=Release"/> </Target> <Target Name="ProjectB"> <MSBuild Projects="$(ProjectBPath)\ProjectB.sln" Properties="Configuration=Release"/> </Target> <Target Name="ProjectC"> <MSBuild Projects="$(ProjectCPath)\ProjectC.sln" Properties="Configuration=Release"/> </Target>
我们看到,我们通过Target的设置成功的将不同工程联系了起来,当我们需要编译FrameworkA时,我们只需要调用FrameworkA这个Target,它会先FrameworkA编译,然后再调用ProjectA,ProjectB,ProjectC的编译。
哈哈,一切准备工作都就绪了,我们需要在MSBuild的扩展Task里完成的任务就是:
1. 读取mods.xml,自动判断入库代码所属工程。
2. 返回需要编译的工程名列表。
我们在VS里建立一个DLL工程,然后添加Microsoft.Build.Utilities和Microsoft.Build.Framework的引用,然后编写我们自定义的Task类,我取名为MyTask,让它继承Task类,我们要做的是重写其中的Execute方法。MSBuild具体的Task写法请参照How to write a Task,我这里不再重复了,下面是的MyTask代码:
MyTask using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Build.Utilities; using System.Xml; using System.Collections; using Microsoft.Build.Framework; using System.IO; namespace CoderZh.MyTask { public class MyTask : Task { [Output] public ITaskItem[] Targets { get; set; } [Required] public ITaskItem[] Projects { get; set; } [Required] public string SvnModifyFile { get; set; } [Required] public string StateFile { get; set; } private DateTime curBuildTime; private DateTime lastBuildTime; private Boolean lastBuildResult = false; /**//// <summary> /// My Task Run From Here /// </summary> /// <returns></returns> public override bool Execute() { if ((this.Projects == null) || (this.Projects.Length == 0)) { return true; } //Read last build time and result this.ReadLastBuildStatus(); if (!this.lastBuildResult || this.lastBuildTime.Day != DateTime.Now.Day) {//If last build fail, or it is another day, then run all the targets Log.LogMessage("Last build fail, or it is another day, then run all the targets"); this.SetAllTargetsToRun(); } else {//check the svn and run the specify targets this.SetTargetsToRunBySvnModify(); } return true; } /**//// <summary> /// Read Last Build Result, Success Or Not /// </summary> private void ReadLastBuildStatus() { try { XmlDocument doc = new XmlDocument(); doc.Load(this.StateFile); XmlNode lastBuildTimeNode = doc.SelectSingleNode("/IntegrationResult/StartTime"); this.lastBuildTime = Convert.ToDateTime(lastBuildTimeNode.InnerText); XmlNode lastBuildResultNode = doc.SelectSingleNode("/IntegrationResult/LastIntegrationStatus"); this.lastBuildResult = lastBuildResultNode.InnerText.ToLower() == "success"; Log.LogMessage("Load from : {0}\r\nLastBuild Time : {1}\r\nLastBuild Result : {2}", this.StateFile, this.lastBuildTime.ToString(), this.lastBuildResult.ToString()); doc = null; } catch(Exception ex) { Log.LogWarningFromException(ex); this.lastBuildTime = DateTime.Today.AddDays(-1.0); this.lastBuildResult = false; } } /**//// <summary> /// Set All targets to run /// </summary> private void SetAllTargetsToRun() { ArrayList list = new ArrayList(); foreach (ITaskItem item in this.Projects) { string targetName = item.GetMetadata("ProjectName"); if (!list.Contains(targetName)) { list.Add(targetName); } } ArrayList targetList = new ArrayList(); foreach (string item in list) { targetList.Add(new TaskItem(item)); } this.Targets = (ITaskItem[])targetList.ToArray(typeof(ITaskItem)); } /**//// <summary> /// Set Targets to run by SVN Modify /// </summary> private void SetTargetsToRunBySvnModify() { this.curBuildTime = DateTime.Now; ArrayList list = new ArrayList(); List<string> mods = GetModification(); foreach (ITaskItem item in this.Projects) { string projectFolder = Path.GetFullPath(item.ItemSpec); string excludeFolder = item.GetMetadata("Exclude"); excludeFolder = String.IsNullOrEmpty(excludeFolder) ? String.Empty : Path.GetFullPath(excludeFolder); Log.LogMessage("\nprojectFolder:" + projectFolder); foreach (string mod in mods) { string modifyFolder = Path.GetFullPath(mod.Replace(@"/trunk", "..")); Log.LogMessage("\t-- modifyFolder:" + modifyFolder); if (modifyFolder.Contains(projectFolder)) { if (!String.IsNullOrEmpty(excludeFolder) && modifyFolder.Contains(excludeFolder)) { Log.LogMessage("Exclude : {0}", excludeFolder); continue; } string targetName = item.GetMetadata("ProjectName"); Log.LogMessage("Matched : {0}", targetName); list.Add(new TaskItem(targetName)); break; } } } this.Targets = (ITaskItem[])list.ToArray(typeof(ITaskItem)); } /**//// <summary> /// Get Modification From mods.xml /// </summary> /// <returns></returns> private List<string> GetModification() { List<string> modList = new List<string>(); try { XmlDocument doc = new XmlDocument(); doc.Load(this.SvnModifyFile); XmlNodeList modNodeList = doc.SelectNodes("/ArrayOfModification/Modification"); foreach (XmlNode modNode in modNodeList) { XmlNode folderNode = modNode.SelectSingleNode("FolderName"); modList.Add(folderNode.InnerText); } doc = null; } catch (Exception ex) { Log.LogWarningFromException(ex); } return modList; } } }
接下来完成最后一步,配置完成我们的MSBuild配置文件。我们添加MyTask相关的内容:
<UsingTask AssemblyFile="CoderZh.MyTask.dll" TaskName="MyTask"/> <Target Name="Build"> <MyTask SvnModifyFile="$(SvnModifyFile)" StateFile="$(CCNetStateFile)" Projects="@(SvnFolder)"> <Output TaskParameter="Targets" ItemName="TargetNames" /> </MyTask> <Message Text="Targets to be call:@(TargetNames)"/> <CallTarget Targets="@(TargetNames)" /> </Target>
OK,搞定!
三、总结
通过上面的方法,我们实现了:
1.CCNET只更新需要编译的工程代码,大大减少了SVN更新的时间,同时,也减少了SVN编译的次数。
2.我们实现了只编译入库代码所属工程,以及其相关联的工程。大大减少了编译工程的范围,缩短了编译时间。
我也知道,上面的解决方案不够完美,也许有更加直接,简单的处理办法,也请大家拿出来讨论讨论,不甚感激。
本文相关的配置文件及代码如下,希望对大家有微薄之助。
代码:/Files/coderzh/mytask/MyBuild.rar
MSBuild 配置文件:/Files/coderzh/mytask/mybuild.txt
CCNET配置文件:/Files/coderzh/mytask/ccnet.txt
本文转载自:http://www.cnblogs.com/coderzh/archive/2009/04/05/1429858.html
CCNET+MSBuild+SVN实时构建的优化总结的更多相关文章
- CCNET+MSBuild+SVN实现每日构建
最近开始将源代码迁移到SVN,于是便考虑到如何从SVN定期获取源码,自动编译并部署以减轻工作量并提高工作效率.通过多方搜集资料并进行研究,基本实现了这个功能.对于每日构建的概念就不具体展开了,可以在各 ...
- Mac下Jenkins+SVN+Xcode构建持续
1 安装Jenkins Jenkins是基于Java开发的一种持续集成工具.所以呢,要使用Jenkins必须使用先安装JDK. JDK安装 JDK 下载地址 jdk 1.8.png 安装JDK的过程略 ...
- Mac下Jenkins+SVN+Xcode构建持续导出环境
1 安装Jenkins Jenkins是基于Java开发的一种持续集成工具.所以呢,要使用Jenkins必须使用先安装JDK. JDK安装 JDK 下载地址 jdk 1.8.png 安装JDK的过程略 ...
- 在.NET 环境中实现每日构建(Daily Build)--ccnet,MSBuild篇(转载)
每日构建,对我们团队来说一个全新的概念.随着项目开发的进展,在开发过 程需要及时反馈一些BUG和功能要求的处理情况.而在这种情况下每天或隔一段时间Build一个版本,工作量还是比较大的,所以就特别有必 ...
- Jenkins+MSbuild+SVN实现快速搭建.net持续集成环境(构建、编辑、部署到服务器)
Jenkins是一个可扩展的持续集成引擎,Jenkins非常易于安装和配置,简单易用,下面开始搭建.net持续集成环境 Jenkins和SVN安装这里就不介绍了 一.准备工作 1.Jenkins中系统 ...
- Jenkins配置MSBuild实现自动部署(MSBuild+SVN/Subversion+FTP+BAT)
所要用到的主要插件: [MSBuild Plugin] 具体操作: 1.配置MSBuild的版本 [系统管理]->[Global Tool Configuration]->[MSBuild ...
- Jenkins+MSbuild+SVN实现dotnet持续集成 快速搭建持续集成环境
Jenkins是一个可扩展的持续集成引擎,Jenkins非常易于安装和配置,简单易用,下面开始搭建dotnet持续集成环境 一.准备工作 1.系统管理-->管理插件-->可选插件中找到MS ...
- (持续集成)win7上部署Jenkins+MSBuild+Svn+SonarQube+SonarQube Scanner for MSBuild (第二发)
这一篇进入实战,走起.... 登录jenkins,如下图 点击上图中的“新建”按钮,进入下图 输入项目名称,选择“构建一个自由风格的软件项目”即可,点击“ok”,跳转到下图 svn源代码管理(选择代码 ...
- (持续集成)win7上部署Jenkins+MSBuild+Svn+SonarQube+SonarQube Scanner for MSBuild (一)
一.Jenkins介绍 jenkins是一个广泛用于持续构建的可视化web工具,持续构建说得更直白点,就是各种项目的”自动化”编译.打包.分发部署.jenkins可以很好的支持各种语言(比如:java ...
随机推荐
- Fedora25
Fedora默认情况是没有装flash的,首先输入https://get.adobe.com/flashplayer/?loc=cn,选择.rpm包,点击立即下载,下载完成后进到download目录, ...
- sendUserActionEvent() mView== null after clicking on button
this is not a problem related to your code, but related to S4 android version. Same question has bee ...
- 反射机制(reflection)动态相关机制
功能:动态获取类的信息以及动态调用对象的方法. Java反射机制主要提供了以下功能: 1.在运行时判断任意一个对象所属的类. 2.在运行时构造任意一个类的对象. 3.在运行时判断任意一个类所具有的成员 ...
- iOS 打包上传AppStore相关(2)-Xcode相应配置
上一篇描述了如何在AppleDeveloper创建Certificates.App IDs和Provisioning Profiles的过程.本篇将详细描述在Xcode部分我们需要做的配置. 1.配置 ...
- 共享AFHTTPSessionManager 单例好处浅析
很多时候,AFNetworking都是目前iOS开发者网络库中的不二选择.Github上2W+的star数足见其流行程度.而从iOS7.0开始,苹果推出了新的网络库继承者NSURLSession后 ...
- Linux下Modules的概念及使用详解[转贴]
一.什么是 modules? modules 的字面意思就是模块,在此指的是 kernel modules:简单来说,一个模块提供了一个功能,如 isofs.minix.nfs.lp 等等.传统来讲, ...
- Docker 安装及问题处理
1 确定Linux版本 uname -r 2 升级系统(添加 APT 镜像源,添加使用 HTTPS 传输的软件包以及 CA 证书.) sudo apt-get update sudo apt-get ...
- Linux服务器操作系统
Linux服务器操作系统 今日大纲 ● 服务器操作系统的系列.Linux的主流产品.虚拟机软件 ● 安装linux ● linux基本命令 ● 用户管理及权限(多用户) ● ...
- ZOJ 1655 FZU 1125 Transport Goods
迪杰斯特拉最短路径. 1.every city must wait till all the goods arrive, and then transport the arriving goods t ...
- Java 泛型 协变式覆盖和泛型重载
Java 泛型 协变式覆盖和泛型重载 @author ixenos 1.协变式覆盖(Override) 在JDK 1.4及以前,子类方法如果要覆盖超类的某个方法,必须具有完全相同的方法签名,包括返回值 ...