本文不是介紹如何使用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.我們實現了只編譯入庫代碼所屬工程,以及其相關聯的工程。大大減少了編譯工程的范圍,縮短了 編譯時間。
我也知道,上面的解決方案不夠完美,也許有更加直接,簡單的處理辦法,也請大家拿出來討論討論 ,不甚感激。
本文相關的配置文件及代碼如下,希望對大家有微薄之助。
MSBuild 配置文件:mybuild.txt
CCNET配置文件:ccnet.txt
出處:http://coderzh.cnblogs.com/
本文配套源碼