首页 > 代码库 > 团队项目利用Msbuild自定义Task实现增量发布
团队项目利用Msbuild自定义Task实现增量发布
最近一直在做自动部署工具,主要利用到了Msbuild的自定义Task,通过Task我们可以自定义编译、部署过程减少人工直接干预。Msbuild的详细用法,可以去园子里搜一下,有很多的基础教程,这里就不赘述了,还是集中说一下增量发布的问题。
增量主要涉及到三部分内容,程序、配置和静态文件(例如CSS、JS等),程序的增量比较简单,通过版本对比或者TFS的修改记录便可以查询出被修改过的程序集。配置文件增量大致有两种,全增量和部分增量。全增量也很简单,直接把修改过的配置文件复制到发布包就OK了;部分增量需要我们比较这个配置里所有修改过的节点、属性,并且只输出这些改动。之前做发布包均是通过VS人工比较两个版本的配置文件,手动COPY出来,再放入到发布包。这样做在配置文件比较少的时候没有问题,但是整个项目的工程较多,配置文件非常多就很麻烦了,每次单独做增量配置文件就要花费很长时间。我们可以利用Task来完成整个项目的增量部署包制作。静态文件的处理通常只是将开发版本进行压缩输出,这个园子里已经有成熟实例了,我们这里就不单独写出来了。
目录
- Hello Task
- TFS 基本操作
- Start 增量发布
- DLL 和 配置文件 增量发布
1. Hello Task
新建一个Library项目,命名为HelloTask,同时新建一个HelloTask类,添加如下引用
using Microsoft.Build.Framework;using Microsoft.Build.Utilities;
添加如下代码
1 public class HelloTask : Microsoft.Build.Utilities.Task2 {3 public override bool Execute()4 {5 Log.LogMessage("Hello Task!");6 return true;7 }8 }
这是再在Solution里添加一个名为HelloTask.Web的空Web项目,用来做实验(其它随便什么类型项目都行,WEB项目主要是后面做增量实验的时候有用),右键单击Publish,新建一个名为HelloTask的部署配置文件,Publish Method选File System,选好发布路径。这时,在Properties会多一个PublishProfiles\HelloTask.pubxml文件,这便是发布的配置,这个也可以直接写入到msbuild的配置里去。修改为如下内容
<?xml version="1.0" encoding="utf-8"?><!--This file is used by the publish/package process of your Web project. You can customize the behavior of this processby editing this MSBuild file. In order to learn more about this please visit http://go.microsoft.com/fwlink/?LinkID=208121. --><Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <UsingTask AssemblyFile="$(MSBuildProjectDirectory)\..\HelloTask.Lib\HelloTask.dll" TaskName="HelloTask"></UsingTask> <PropertyGroup> <WebPublishMethod>FileSystem</WebPublishMethod> <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration> <LastUsedPlatform>Any CPU</LastUsedPlatform> <SiteUrlToLaunchAfterPublish /> <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish> <ExcludeApp_Data>False</ExcludeApp_Data> <publishUrl>Publish</publishUrl> <DeleteExistingFiles>False</DeleteExistingFiles> </PropertyGroup> <Target Name="HelloTask" AfterTargets="GatherAllFilesToPublish"> <HelloTask></HelloTask> </Target></Project>
注意AssemblyFile的路径,需要指向HelloTask.dll的输出目录,这里我在Output里修改过。然后右键选中Web项目,选择Publish, 输出窗口便可以看到输出,同时在设置的发布路径下可以看到整个Web项目的输出。
要实现自定义Task,我们主要需要实现Microsoft.Build.Framework.dll里面ITask接口,其定义如下
namespace Microsoft.Build.Framework{ public interface ITask : object { bool Execute(); IBuildEngine BuildEngine { get; set; } ITaskHost HostObject { get; set; } }}
BuildEngine定义了编译引擎接口,HostObject定义了编译的宿主信息,这里Microsoft.Build.Utilities里为我们实现了这个接口,我们只需要重写Execute这个方法就行。上面就实现了一个非常简单的输出Hello Task的功能。
2. TFS 基本操作
虽然现在Git满天飞,但是在做.Net项目是我们还是依然再用TFS,毕竟和VS集成度最好。这里需要使用TFS API来对项目和配置文件的修改状态进行判断,从而实现增量输出。在Task项目里再添加如下引用
using Microsoft.TeamFoundation.Client;using Microsoft.TeamFoundation.VersionControl.Client;
然后我们分别介绍下后面需要用到的操作,其他TFS的API童鞋们自行研究吧,
public static VersionControlServer Open(string path) { var info = Workstation.Current.GetLocalWorkspaceInfo(path); var uri = info.ServerUri; //var uri = "..." var tfsCollection = new TfsTeamProjectCollection(uri); return tfsCollection.GetService<VersionControlServer>(); }
这里在获取VersionControlServer实例的时候转了下弯,其实可以直接输入服务器的uri,但是这里首先获取了本地的代码仓库,从本地的代码仓库再获取到了服务器的uri;同时TfsTeamProjectCollection有多个重载,可以显示的提供登陆凭据,这里我们偷懒,直接利用系统里已经存好的凭据登陆,具体可以查看
返回的VersionControlServer实例是后续操作的基础,因此在这个静态类的我们将它放入
public static VersionControlServer SourceControl { get; set; }
TFS里,每一次Check-In,会提交一个Changeset,里面包含了本次改动的所有Change,这些Change都对应到Item的,即我们版本管理的每一个文件。我们发布时,通常的做法是在当前版本的代码上打上相应的标注Label,使其成为一个特定的版本。那么,在增量发布中,我们就可以通过这些特性的标注来获取版本间的差别。创建标注的代码如下
public static void CreateLabel(string scope, string label) { var itemSpec = new ItemSpec(scope, RecursionType.Full); var labelItemSpec = new LabelItemSpec(itemSpec, VersionSpec.Latest, false); var vslabel = new VersionControlLabel(SourceControl, label, SourceControl.AuthorizedUser, scope, label); SourceControl.CreateLabel(vslabel, new[] { labelItemSpec }, LabelChildOption.Replace); }
scope为这个标注的范围,通常我们以解决方案为发布的基本单元,那么传入这个解决方案的目录就可以了。查询标注的代码如下
public static VersionControlLabel QueryLabel(string scope, string label) { return SourceControl.QueryLabels(label, null, null, true, scope, VersionSpec.Latest).FirstOrDefault(); }
如果要查询所有标注,第一个参数可以传入null。有了这些标注的操作,我们就可以获取这些特定版本之间的的Changeset了
public static IEnumerable<Changeset> Changes(string scope, string label1, string label2 ) { var vsLabel1 = QueryLabel(scope,label1); var vsLabel2 = QueryLabel(scope, label2); var vsLabelSpec1 = new LabelVersionSpec(vsLabel1.Name, vsLabel1.Scope); var vsLabelSpec2 = new LabelVersionSpec(vsLabel2.Name, vsLabel2.Scope); return SourceControl.QueryHistory(vsLabelSpec1.Scope, VersionSpec.Latest, 0, RecursionType.Full, null, vsLabelSpec1, vsLabelSpec2, int.MaxValue, true, false) .Cast<Changeset>(); }
这里需要注意的是label1的版本一定要比label2的版本新,否则在调用QueryHistory的时候是没有返回的。我们还要用到的一个方法是获取特定标注下的某个文件,
public static Item GetSpecVersion(string scope, string label, Item item) { var vslabel = QueryLabel(scope,label); return SourceControl.GetItem(item.ServerItem, new LabelVersionSpec(vslabel.Name, vslabel.Scope)); }
有了这些方法,我们就可以开始实现增量发布了。
3. Start增量发布
正经Coding之前我们做点准备工作,方便调试。在Solution里在添加一个HelloTask.Task.Debug的Console项目,加入一个批处理文件debug.bat,内容如下
@echo offC:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild "D:\HelloTask\HelloTask.Web\HelloTask.Web.csproj" /t:GatherAllFilesToPublish /p:PublishProfile=HelloTask;SolutionDir=D:\HelloTask\ /v:q
然后修改其属性Copy to Output Direcotry -> Coy always,同时修改Debug项目的输出目录到HelloTask.dll的目录。然后修改在Execute()加入调试代码
#if DEBUG System.Diagnostics.Debugger.Launch();#endif
还需要修改整个Solution的启动项目为HelloTask.Task.Debug,这样使用F5我们就可以直接调试程序了。批处理是执行IDE里面右键Publish的功能,Console是为了给Solution提供一个启动的项目,这里用Process去调用批处理主要是为了瞬间让IDE退出HelloTask.Task.Console的执行,从而准备好进入到HelloTask的调试,这样不用开多个IDE了。
同时为了配合增量功能的实现,我们继续修改发布的配置文件HelloTask.pubxml,增加修改如下内容
<ItemGroup> <Label Include="label1"> <From>lable2</From> </Label></ItemGroup><Target Name="HelloTask" AfterTargets="GatherAllFilesToPublish"> <HelloTask SolutionDir="$(MSBuildProjectDirectory)\..\" Version="@(Label)"></HelloTask></Target>
再定义一些其他必要的结构如 ProjectItem 和 ChangedItem
public class ProjectItem { public static List<ProjectItem> ProjectCollection { get; set; } public static void GetAll(string solutionDir) { ProjectCollection = new List<ProjectItem>(); Directory.GetFiles(solutionDir, "*.csproj", SearchOption.AllDirectories) .ToList() .ForEach(t => ProjectCollection.Add(new ProjectItem(t))); } public static ProjectItem Find(Item item) { return ProjectCollection.ToList() .FirstOrDefault(t => item.ServerItem.IndexOf(t.Name) >= 0); } public string Name { get; set; } public string Path { get; set; } public string AssemblyName { get; set; } public bool Changed { get; set; } public string OutputType { get; set; } public ProjectItem(string path) { Path = path; Name = System.IO.Path.GetFileNameWithoutExtension(path); var doc = new XmlDocument(); doc.Load(path); var ns = new XmlNamespaceManager(doc.NameTable); ns.AddNamespace("ns", "http://schemas.microsoft.com/developer/msbuild/2003"); var node = doc.SelectSingleNode("//ns:PropertyGroup//ns:OutputType", ns); OutputType = node!=null? node.InnerText :string.Empty; node=doc.SelectSingleNode("//ns:PropertyGroup//ns:AssemblyName", ns); AssemblyName = node != null ? node.InnerText : string.Empty; this.Changed =false; } }
public class ChangedItem { public ChangedItem(ChangeType changeType, Microsoft.TeamFoundation.VersionControl.Client.Change change) { ChangeType = changeType; Change = change; } public ChangeType ChangeType { get; set; } public Microsoft.TeamFoundation.VersionControl.Client.Change Change { get; set; } public static IEnumerable<ChangedItem> Build(IEnumerable<Changeset> changesets) { var list = new List<ChangedItem>(); changesets = changesets.OrderBy(t => t.CreationDate); var changes = new List<Microsoft.TeamFoundation.VersionControl.Client.Change>(); changesets.ToList().ForEach(changeset => changes.AddRange(changeset.Changes)); changes.Distinct(new ChangeComparer()) .ToList() .ForEach(change =>{ var changeType = ChangeType.None; changes.Where(t => t.Item.ItemId == change.Item.ItemId) .ToList() .ForEach(t => changeType = changeType | t.ChangeType); list.Add(new ChangedItem(changeType, change)); }); return list; } } public class ChangeComparer : IEqualityComparer<Microsoft.TeamFoundation.VersionControl.Client.Change> { public bool Equals(Microsoft.TeamFoundation.VersionControl.Client.Change x, Microsoft.TeamFoundation.VersionControl.Client.Change y) { return x.Item.ItemId == y.Item.ItemId; } public int GetHashCode(Microsoft.TeamFoundation.VersionControl.Client.Change obj) { return obj.Item.ItemId.GetHashCode(); } }
主要说一下ChangedItem吧,因为我们在label之间查询返回的是多个Changeset,因此可能会返回一个文件的多次修改状态,因此,我们需要将这些状态组合在一起来判断两个label之间这些文件的最终状态,MS刚好提供了ChangeType这个这个Flags的枚举,我要做的只用将同一个文件的多个ChangeType进行或操作。
然后定义了接口IAdditionable
public interface IAdditionable { void Republish(string publishFolder, string tempFolder); }
同时有一个默认的实现DefaultAddition
public class DefaultAddition : IAdditionable { public ChangedItem ChangedItem { get; set; } public ProjectItem ProjectItem { get; set; } public DefaultAddition(ChangedItem changedItem) { this.ChangedItem = changedItem; this.ProjectItem = ProjectItem.Find(changedItem.Change.Item); } public virtual void Republish(string publishFolder,string tempFolder) { FileUtility.CopyTo(GetAbsolutePath(publishFolder), GetAbsolutePath(tempFolder)); } protected string GetRelativePath() { var start = ChangedItem.Change.Item.ServerItem.IndexOf(ProjectItem.Name) + ProjectItem.Name.Length; return ChangedItem.Change.Item.ServerItem.Substring(start).Trim(‘/‘).Trim(‘\\‘); } protected string GetAbsolutePath(string dir) { return Path.Combine(dir, GetRelativePath()); } }
默认的增量发布直接将文件Copy过去,这里实现的Republish标明为virtual类型,因为后面针对不同的的类型,我们将会直接继承DefaultAddition这个,同时重写Republish这个方法。FileUtility里提供了一些安全的文件操作方法,这个就懒得贴了。
4. DLL 和 配置文件 增量发布
然后首先实现一个个简单的DLL增量,DLL增量无非是查找哪些项目里的*.cs文件被修改过(这里我们不考虑极端情况),改过我们就像这个项目的DLL添加到增量发布包里面。因此新建一个类ProjectAddition,具体实现如下,
public class ProjectAddition : DefaultAddition { public ProjectAddition(ChangedItem changedItem) : base(changedItem) { this.ChangedItem = changedItem; } public override void Republish(string publishFolder, string tempFolder) { var bin = "bin"; var assembly = string.Format("{0}.{1}", ProjectItem.AssemblyName, ProjectItem.OutputType == "Library" ? "dll" : "exe"); var pdb = string.Format("{0}.pdb", ProjectItem.AssemblyName); var assemblyFrom = Path.Combine(publishFolder, bin, assembly); var assemblyTo = Path.Combine(tempFolder, bin, assembly); var pdbFrom = Path.Combine(publishFolder, bin, pdb); var pdbTo = Path.Combine(tempFolder, bin, pdb); FileUtility.CopyTo(assemblyFrom,assemblyTo); FileUtility.CopyTo(pdbFrom,pdbTo); } }
因为发布的是Web项目,所以所有的dll都放在bin目录下,这里为了方便路径直接写在了里方法里,其实应该写在配置,同时如果输出的时候有pdb调试文件,也会自动复制。然后我们需要一个工厂方法来对增量的类型进行构造,再添加AdditionManger
public class ChangeManger { static ChangeManger() { ChangeCollection = new List<IAdditionable>(); } public static List<IAdditionable> ChangeCollection { get; set; } public static void AddChange(ChangedItem changeItem) { var ext = Path.GetExtension(changeItem.Change.Item.ServerItem).ToLower(); IAdditionable changed = null; switch (ext) { case ".cs": changed = new ProjectAddition(changeItem); break; case ".config": changed = new ConfigurationAddition(changeItem); break; default: changed = new DefaultAddition(changeItem); break; } if (!ChangeCollection.Exists(t => t.Equals(changed))) ChangeCollection.Add(changed); } public static void AdditionChange() { var tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); var publishFolder = @"D:\HelloTask\HelloTask.Web\Publish"; ChangeCollection.ForEach(t => t.Republish(publishFolder, tempFolder)); } }
这里一并给出了后面将要实现的ConfigurationAddition,这里.cs文件会根据它的项目信息返回前面提到的 ProjectItem实例,同时后面保证了在ChangeCollection里每个项目是唯一的。 AdditionChange这个方法则批量调动了增量接口。其中,里面的publishFolder也应该写入配置,这里偷懒了下。
配置的增量发布实现起来稍微麻烦点,同时要向完全自动化还有点距离,因为在XML里面,对于一个List对象的修改操作本身存在二义性,我们无法准确的获知这个节点的的修改状态,因此我们这里对配置增量的策略是将所有的有修改的节点增量输出,同时将List类型的对象整体输出。
一个测试例子如下,
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <singleNode>1</singleNode> <complexNode> <unchange>content</unchange> <changed>11</changed> </complexNode> <links> <link> <name>百度</name> <url>www.baidu.com</url> </link> <link> <name>Google1</name> <url>http://www.google.com</url> </link> <link> <name>Bing</name> <url>http://www.bing.com</url> </link> </links> </Configuration>
修改后的配置如下,
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <singleNode>1</singleNode> <complexNode> <unchange>content</unchange> <changed>22</changed> </complexNode> <links> <link> <name>百度</name> <url>http://www.baidu.com</url> </link> <link> <name>Google</name> <url>http://www.google.com</url> </link> </link> <link> <name>Bing</name> <url>http://www.bing.com</url> </link> </links> </Configuration>
期待的增量配置如下,
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <complexNode> <changed>22</changed> </complexNode> <links> <link> <name>百度</name> <url>http://www.baidu.com</url> </link> <link> <name>Google</name> <url>http://www.google.com</url> </link> </links> </Configuration>
这里的link对象虽然分别只修改了一个元素的内容,但是我们将整个link全部输出,是为了排除可能存在的二义性,带来的问题是如果这样的link节点较大,输出的增量配置文件也会较大,但是毕竟作为增量配置,需要修改的内容明确。
然后具体实现如下,简单说来通过对配置元素层级递归,每一层对两个配置之间相互求差集,在将差集进行增量输出
public class ConfigurationAddition : DefaultAddition { public ConfigurationAddition(ChangedItem changedItem) : base(changedItem) { this.ChangedItem = changedItem; } public override void Republish(string publishFolder, string tempFolder) { if (ChangedItem.ChangeType.HasFlag(ChangeType.Add)) { base.Republish(publishFolder, tempFolder); return; } var docAddit = new XmlDocument(); var docThis = new XmlDocument(); docThis.Load(GetAbsolutePath(publishFolder)); var itemSpec = TfsUtility.GetSpecVersion(this.Scope,this.Label,this.ChangedItem.Change.Item); var docSpec = new XmlDocument(); docSpec.Load(itemSpec.DownloadFile()); //导入XML Declaration if (docThis.FirstChild.NodeType == XmlNodeType.XmlDeclaration) docAddit.AppendChild(docAddit.ImportNode(docThis.FirstChild, true)); //设置DOcumentElement为增量文档的第一个节点 var nodeThis = (XmlNode)docThis.DocumentElement; var nodeSpec = (XmlNode)docSpec.DocumentElement; var nodeAddit = docAddit.ImportNode(nodeThis, true); RecursiveCompareChildNode(nodeThis, nodeSpec, nodeAddit, docAddit); docAddit.AppendChild(nodeAddit); var path = Path.ChangeExtension(GetAbsolutePath(tempFolder), ".addition.config"); FileUtility.CreateIfNotExists(path); docAddit.Save(path); } private void RecursiveCompareChildNode(XmlNode nodeThis, XmlNode nodeSpec, XmlNode nodeAddit, XmlDocument docAddit) { //如果当前节点是LIST对象,直接输出整个节点 if (nodeThis.ParentNode != null && nodeThis.ParentNode.ChildNodes.Cast<XmlNode>() .Count(t => t.Name == nodeThis.Name && t.NodeType == XmlNodeType.Element) > 1) return; nodeAddit.InnerXml = string.Empty; var listThis = nodeThis.ChildNodes.Cast<XmlNode>().ToList(); var listSpec = nodeSpec.ChildNodes.Cast<XmlNode>().ToList(); //*完全一样的elements会被忽略掉 var comparer = new XmlNodeComparer(); var exceptsThis = listThis.Except(listSpec, comparer).ToList(); var exceptsSpec = listSpec.Except(listThis, comparer).ToList(); foreach (var exceptNode in exceptsThis) { var childAddit = docAddit.ImportNode(exceptNode, true); //如没有element子节点,直接加入到增量文档 if (exceptNode.ChildNodes.Cast<XmlNode>().All(t => t.NodeType != XmlNodeType.Element)) { nodeAddit.AppendChild(childAddit); continue; } var childSpec = nodeSpec.ChildNodes .Cast<XmlNode>() .ToList() .FirstOrDefault(t => NodePathCompare(t, exceptNode)); nodeAddit.AppendChild(childAddit); } foreach (var exceptNode in exceptsSpec) { if (!exceptsThis.Exists(t => t.Name == exceptNode.Name)) nodeAddit.AppendChild(docAddit.ImportNode(exceptNode, true)); } } private bool NodePathCompare(XmlNode node1, XmlNode node2) { var node1path = node1.Name.GetHashCode(); var node2path = node2.Name.GetHashCode(); var node = node1; while (node.ParentNode != null) { node = node.ParentNode; node1path += node.Name.GetHashCode(); } node = node2; while (node.ParentNode != null) { node = node.ParentNode; node2path += node.Name.GetHashCode(); } return node1path == node2path; } }//class
最后在我们的HelloTask里对这个AdditionManger进行调用就行了。目前方法可以输出两个配置之间的差异,但是还不能确定对这个节点具体的操作,后续如果研究出来将会补上。