本文不是介绍如何使用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实时构建的优化总结的更多相关文章

  1. CCNET+MSBuild+SVN实现每日构建

    最近开始将源代码迁移到SVN,于是便考虑到如何从SVN定期获取源码,自动编译并部署以减轻工作量并提高工作效率.通过多方搜集资料并进行研究,基本实现了这个功能.对于每日构建的概念就不具体展开了,可以在各 ...

  2. Mac下Jenkins+SVN+Xcode构建持续

    1 安装Jenkins Jenkins是基于Java开发的一种持续集成工具.所以呢,要使用Jenkins必须使用先安装JDK. JDK安装 JDK 下载地址 jdk 1.8.png 安装JDK的过程略 ...

  3. Mac下Jenkins+SVN+Xcode构建持续导出环境

    1 安装Jenkins Jenkins是基于Java开发的一种持续集成工具.所以呢,要使用Jenkins必须使用先安装JDK. JDK安装 JDK 下载地址 jdk 1.8.png 安装JDK的过程略 ...

  4. 在.NET 环境中实现每日构建(Daily Build)--ccnet,MSBuild篇(转载)

    每日构建,对我们团队来说一个全新的概念.随着项目开发的进展,在开发过 程需要及时反馈一些BUG和功能要求的处理情况.而在这种情况下每天或隔一段时间Build一个版本,工作量还是比较大的,所以就特别有必 ...

  5. Jenkins+MSbuild+SVN实现快速搭建.net持续集成环境(构建、编辑、部署到服务器)

    Jenkins是一个可扩展的持续集成引擎,Jenkins非常易于安装和配置,简单易用,下面开始搭建.net持续集成环境 Jenkins和SVN安装这里就不介绍了 一.准备工作 1.Jenkins中系统 ...

  6. Jenkins配置MSBuild实现自动部署(MSBuild+SVN/Subversion+FTP+BAT)

    所要用到的主要插件: [MSBuild Plugin] 具体操作: 1.配置MSBuild的版本 [系统管理]->[Global Tool Configuration]->[MSBuild ...

  7. Jenkins+MSbuild+SVN实现dotnet持续集成 快速搭建持续集成环境

    Jenkins是一个可扩展的持续集成引擎,Jenkins非常易于安装和配置,简单易用,下面开始搭建dotnet持续集成环境 一.准备工作 1.系统管理-->管理插件-->可选插件中找到MS ...

  8. (持续集成)win7上部署Jenkins+MSBuild+Svn+SonarQube+SonarQube Scanner for MSBuild (第二发)

    这一篇进入实战,走起.... 登录jenkins,如下图 点击上图中的“新建”按钮,进入下图 输入项目名称,选择“构建一个自由风格的软件项目”即可,点击“ok”,跳转到下图 svn源代码管理(选择代码 ...

  9. (持续集成)win7上部署Jenkins+MSBuild+Svn+SonarQube+SonarQube Scanner for MSBuild (一)

    一.Jenkins介绍 jenkins是一个广泛用于持续构建的可视化web工具,持续构建说得更直白点,就是各种项目的”自动化”编译.打包.分发部署.jenkins可以很好的支持各种语言(比如:java ...

随机推荐

  1. [ An Ac a Day ^_^ ] [kuangbin带你飞]专题五 并查集 POJ 2236 Wireless Network

    题意: 一次地震震坏了所有网点 现在开始修复它们 有N个点 距离为d的网点可以进行通信 O p   代表p点已经修复 S p q 代表询问p q之间是否能够通信 思路: 基础并查集 每次修复一个点重新 ...

  2. 如何通过subId来获取phoneId?

    androidL中使用一张数据表来保存sim卡信息:telephony.db中有一张记录SIM卡信息的表,siminfo: CREATE TABLE siminfo(_id INTEGER PRIMA ...

  3. Visual Studio下使用jQuery的10个技巧

    广泛流行的jQuery是一个开源的,跨浏览器和兼容CSS 3的JavaScript库,你可以用它简化你的JavaScript编码任务和操作(添加,编辑和删除)HTML内容中的DOM元素,本文介绍10个 ...

  4. C#模板打印功能-模板为WPS或Excel

    //---WPS----- using EtApp = ET; using System.Reflection; using System.Runtime.InteropServices; using ...

  5. Eclipse中集成Perforce插件

    perforce插件是由perforce官方提供的,安装时需要依赖DLTK,如下: 第一步:打开你的Eclipse,然后 Help -> Install New Software ... -&g ...

  6. HTML link标签media参数

    写html这么久了,今天才发现link标签还有个media参数,赶紧把它补回来,虽然现在没有用到,但是不能不知道它 定义和用法 media 属性规定被链接文档将显示在什么设备上. media 属性用于 ...

  7. CentOS 下网络报错 Device eth0 does not seem to be present

    在执行network服务重启后,出现Device eth0 does not seem to be present  问题,主要是因为系统之前有多个网卡配置,和配置文件不匹配造成的. 解决这种问题,思 ...

  8. 一个小时快速搭建微信小程序

    「小程序」这个划时代的产品发布快一周了,互联网技术人都在摩拳擦掌,跃跃欲试.可是小程序目前还在内测,首批只发放了 200 个内测资格(泪流满面).本以为没有 AppID 这个月就与小程序无缘了,庆幸的 ...

  9. 如何安装使用Impala

      一.Impala简介 Cloudera Impala对你存储在Apache Hadoop在HDFS,HBase的数据提供直接查询互动的SQL.除了像Hive使用相同的统一存储平台,Impala也使 ...

  10. URAL 1525 Path

    #include<stdio.h> #include<string.h> #include<math.h> #include<algorithm> us ...