程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> CCNET+MSBuild+SVN實時構建的優化總結

CCNET+MSBuild+SVN實時構建的優化總結

編輯:關於.NET

本文不是介紹如何使用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/

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved