CCNET+MSBuild+SVN实时构建的优化总结

原文作者 作者:CoderZhCoderZh的技术博客 - 博客园,原文地址 http://www.cnblogs.com/coderzh/archive/2009/04/05/1429858.html




本文不是介绍如何使用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代码:


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 getset; }

        [Required]
        
public ITaskItem[] Projects getset; }

        [Required]
        
public string SvnModifyFile getset; }

        [Required]
        
public string StateFile getset; }

        
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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值