MSBuild Zip File, SVN Revision after Web Publish


As part of an automation part for deploying an application I’ve spent some time creating MSBuild scripts for simple tasks. The following are the tasks I’ll be providing

  • Create a Zip file from the output
  • Read the SVN Revision from the repository

The code will be using inline tasks, since they can be created almost at any time. The inline tasks, lets the user create tasks using C# (and compile against .NET 4.0), at least by the time I am creating this blog post.

Reading the Revision Number using MSBuild

For this particular MSBuild task, I am depending on a third party assembly, SharpSvn. It is a SVN client implement in C#. The tricky part in this script is about loading external assemblies and calling its functions, all the work is doing thru reflection.

<UsingTask TaskName="SVNRevisionReader"               TaskFactory="CodeTaskFactory"               AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
    <ParameterGroup>
      <SvnPath ParameterType="System.String" Required="true" />
	  <SharpSvnUIDllFilePath ParameterType="System.String" Required="false" />
	  <SvnOutputDirectory ParameterType="System.String" Required="false" />
	  <LastChangeRevision ParameterType="System.Int32" Output="true" />
      <Revision ParameterType="System.Int32" Output="true" />
    </ParameterGroup>
    <Task>
		<Reference Include="Microsoft.CSharp" />
		<Using Namespace="System"/>
		<Using Namespace="System.Diagnostics"/>
		<Using Namespace="System.IO"/>
		<Using Namespace="System.Reflection"/>
		<Code Type="Fragment" Language="cs">
<![CDATA[
            //========================= BEGIN: In-line Task
            Func<string, Assembly> loadAssembly = (string assemblyFilePath) =>
            {
                Assembly assembly = null;
				Log.LogMessage("Loading assembly from :" + assemblyFilePath, MessageImportance.High);
                if (!string.IsNullOrEmpty(assemblyFilePath) && System.IO.File.Exists(assemblyFilePath))
                {
                    assembly = Assembly.UnsafeLoadFrom(assemblyFilePath);
                }
                else
                {
                    Log.LogError("Assembly file path:" + assemblyFilePath + " does not exist.", MessageImportance.High);
                }

                return assembly;
            };

            var sharpSvnAssembly = loadAssembly(@"..\MSBuild\SharpSvn.1.9-x86.1.9004.3913.141\lib\net40\SharpSvn.dll");
            //var sharpSvnUIAssembly = loadAssembly(@"..\MSBuild\SharpSvn.1.9-x86.1.9004.3913.141\lib\net40\SharpSvn.UI.dll");
            var typeOfSharpSvnSvnClient = sharpSvnAssembly.GetType("SharpSvn.SvnClient");
            var typeOfSharpSvnSvnTarget = sharpSvnAssembly.GetType("SharpSvn.SvnTarget");
            var typeOfSharpSvnSvnInfoEventArgs = sharpSvnAssembly.GetType("SharpSvn.SvnInfoEventArgs");

            LastChangeRevision = 0;
            Revision = 0;
            //using (var client = new SharpSvn.SvnClient())
            using (dynamic client = Activator.CreateInstance(typeOfSharpSvnSvnClient))
            {
                //SharpSvn.SvnInfoEventArgs info;
                try
                {
                    //client.GetInfo(SharpSvn.SvnTarget.FromString(SvnPath), out info);
                    dynamic svnTarget = sharpSvnAssembly
                            .GetType(@"SharpSvn.SvnTarget")
                            .GetMethod(@"FromString", BindingFlags.Public | BindingFlags.Static, Type.DefaultBinder, new[] { typeof(string) }, null)
                            .Invoke(null, new object[] { SvnPath });
                    var getInfoArgs = new object[] { svnTarget, null };
                    typeOfSharpSvnSvnClient
                        .GetMethod(@"GetInfo", new[] { typeOfSharpSvnSvnTarget, typeOfSharpSvnSvnInfoEventArgs.MakeByRefType() })
                        .Invoke(client, getInfoArgs);
                    dynamic info = getInfoArgs[1];

                    LastChangeRevision = Convert.ToInt32(info.LastChangeRevision.ToString());
                    Revision = Convert.ToInt32(info.Revision.ToString());
                }
                //catch (SharpSvn.SvnInvalidNodeKindException svnInvalidNodeKindException)
                catch (System.Exception exception)
                {
                    Log.LogError(exception.Message);
                }
            }

			if (!string.IsNullOrEmpty(SvnOutputDirectory) && System.IO.Directory.Exists(SvnOutputDirectory))
			{
				System.IO.File.WriteAllLines(System.IO.Path.Combine(SvnOutputDirectory, "LastChangeRevision.svn"), new[] { LastChangeRevision.ToString() });
				System.IO.File.WriteAllLines(System.IO.Path.Combine(SvnOutputDirectory, "Revision.svn"), new[] { Revision.ToString() });
			}

            Log.LogMessage(string.Concat("LastChangeRevision: ", LastChangeRevision, " Revision: ", Revision), MessageImportance.High);
            //========================= END: In-line Task
]]>
      </Code>
    </Task>
  </UsingTask>

Compressing a directory into a Zip file

The next MSBuild script is about using compressing a directory (normally the output directory after the app has been built).

<UsingTask TaskName="ZipDirectory" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
  <ParameterGroup>
	<InputDirectory ParameterType="System.String" Required="true" />
    <OutputFilename ParameterType="System.String" Required="true" />
  </ParameterGroup>
  <Task>
    <Reference Include="System.IO.Compression" />
    <Using Namespace="System.IO.Compression" />
    <Code Type="Fragment" Language="cs">
    <![CDATA[       try       { 		Log.LogMessage(string.Concat("Compressing: ", InputDirectory, " To File: ", OutputFilename), MessageImportance.High); 		 		using (Stream zipStream = new FileStream(Path.GetFullPath(OutputFilename), FileMode.Create, FileAccess.Write)) 		using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) 		{ 			foreach(var filePath in System.IO.Directory.GetFiles(InputDirectory,"*.*",System.IO.SearchOption.AllDirectories)) 			{ 				var relativePath = filePath.Replace(InputDirectory,string.Empty); 				using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))                 using (Stream fileStreamInZip = archive.CreateEntry(relativePath).Open())                     fileStream.CopyTo(fileStreamInZip); 			} 		} 		 		//System.IO.Compression.ZipFile.CreateFromDirectory(InputDirectory, OutputFilename, System.IO.Compression.CompressionLevel.Fastest, true);         return true;       }       catch (Exception ex)       {         Log.LogErrorFromException(ex);         return false;       }     ]]>
    </Code>
  </Task>
</UsingTask>

The previous code snippets are good enough to get the tasks ready for being used. Depending on the type of project, or the needs the MSBuild tasks can be invoked on different times.
 

Calling the SVN Reader before starting the build process starts

In order call your Task before the build process starts, it can be done by specifying the BeforeTargets attribute, and setting it to: PrepareForBuild

<Target Name="EnsureSVNRevision" BeforeTargets="PrepareForBuild">
    <SVNRevisionReader SvnPath="$(SolutionDir)" SvnOutputDirectory="$(MSBuildProjectDirectory)">
      <Output PropertyName="SvnLastChangeRevision" TaskParameter="LastChangeRevision" />
      <Output PropertyName="SvnRevision" TaskParameter="Revision" />
    </SVNRevisionReader>
  </Target>

 

Calling the Directory Zip Compressor after the Build is over

In the case trying to execute the zip compression after building, normally it can be called in the Target AfterBuild, e.g.:

  <Target Name="AfterBuild"
		  Condition="'$(Configuration)' == 'Development' Or '$(Configuration)' == 'Stage'"
	>
    <MakeDir Directories="$(SolutionDir).Deployments/$(Configuration)/" Condition="!Exists('$(SolutionDir).Deployments/$(Configuration)/')" />
    <Delete Files="$(SolutionDir).Deployments/$(Configuration)/$(ProjectName).zip" Condition="Exists('$(SolutionDir).Deployments/$(Configuration)/$(ProjectName).zip')" />
    <ZipDirectory OutputFilename="$(SolutionDir).Deployments/$(Configuration)/$(ProjectName).zip" InputDirectory="$(TargetDir)\" />
  </Target>

 

However if you are creating a Web Project, normally you don’t take the output directory because it’s slightly different from the published files (which take few more processing). Web Projects are published, In this case, it’s assumed that it’s published to the local file system (where the MSBuild has access to it). Thus, The AfterTargets can be used to detect when files are ready

<Target Name="AfterWebPublish" AfterTargets="WebPublish"
          Condition="'$(Configuration)' == 'Development' Or '$(Configuration)' == 'Stage'"
  >
    <MakeDir Directories="$(SolutionDir).Deployments/$(Configuration)/" Condition="!Exists('$(SolutionDir).Deployments/$(Configuration)/')" />
    <Delete Files="$(SolutionDir).Deployments/$(Configuration)/$(ProjectName).zip" Condition="Exists('$(SolutionDir).Deployments/$(Configuration)/$(ProjectName).zip')" />
    <ZipDirectory OutputFilename="$(SolutionDir).Deployments/$(Configuration)/$(ProjectName).zip" InputDirectory="$(MSBuildProjectDirectory)\$(publishUrl)\" />
    <RemoveDir Directories="$(SolutionDir).Deployments/$(Configuration)/$(ProjectName)/" Condition="Exists('$(SolutionDir).Deployments/$(Configuration)/$(ProjectName)/')" />
  </Target>

With the previous snippets it is possible to customize more in detail when the Tasks should be executed to get the work done.

Cheers,
Herb

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s