DD4T: Creating deployment packages

DD4T, Tridion, Continuous Integration, MSBuild, MSDeploy

Do you remember Tridion Deployments to be copy paste tasks, Running Content Porter to transport to your Test, Acceptation and Production environments? Always forgetting some dependency?
DLL files, css, js files and other static dependencies need to be copied from one environment to another. Creating large installation instruction documents. It is deployment hell...

Let's make it a bit easier

This post will describe how to set up the basics. It will explain how to automatically fill your bin folder with your configuration, extra dll files and the Tridion jar files. It will also include the files in your Web Deploy package.

In a next post I will show how to use MSDeploy to deploy the package this post is creating, setup write access to the BinaryData folder automatically during deployment and I will show you how to setup parameters so you can reconfigure your settings during deployment.

Tools I'm using

Directory Structure

First install the DD4T Visual Studio Template if you haven't done so already (Don't place any dll's into the bin folder as instructed, I'll get to that later.). After that open up the directory where your solution file is at. Start by creating the following structure within that folder:

  • Dependencies
    • Dll
    • JarFiles
  • Configuration
    • Tridion Configuration Files

In the Dll directory we'll place at least our xmogrt.dll file. In the JarFiles directory our Tridion jar dependencies. In the Tridion Configuration Folder our, yeah you already know, the Tridion configuration files.

Now open up the csproj file with a text editor (eg. sublime). Go to the very bottom and add this part just within the project tag:

<PropertyGroup>
    <!-- Where to find what -->
    <DependencyDirectory>..\Dependencies</DependencyDirectory>
    <DLLDirectory>$(DependencyDirectory)\Dll</DLLDirectory>
    <JarDirectory>$(DependencyDirectory)\JarFiles</JarDirectory>
    <ConfigurationDirectory>..\Configuration</ConfigurationDirectory>
    <TCMConfigurationDirectory>$(ConfigurationDirectory)\Tridion Configuration Files</TCMConfigurationDirectory>
    <WorkDirectory>..\Work</WorkDirectory>
</PropertyGroup>
<Target Name="EnsureDD4TDependencies" BeforeTargets="PrepareForBuild">
    <ItemGroup>
        <!-- The Tridion Jar files we need -->
        <JarFiles Include="$(JarDirectory)\**\*"/>
        <!-- Our dll dependencies which can't be referenced or don't want to reference -->
        <DllFiles Include="$(DLLDirectory)\xmogrt.dll"/>
        <!-- Our cd_*_conf.xml files etc... -->
        <ConfigFiles Include="$(TCMConfigurationDirectory)\**\*"/>
    </ItemGroup>
    <!-- Create our work directory -->
    <MakeDir Directories="$(WorkDirectory)" />
    <!-- Copy our dll files -->
    <Copy SourceFiles="@(DllFiles)" 
        DestinationFiles="@(DllFiles->'$(WorkDirectory)\bin\%(RecursiveDir)%(Filename)%(Extension)')" />
    <!-- Copy config files -->
    <Copy SourceFiles="@(ConfigFiles)" 
        DestinationFiles="@(ConfigFiles->'$(WorkDirectory)\bin\config\%(RecursiveDir)%(Filename)%(Extension)')" />
    <!-- Copy jar files -->
    <Copy SourceFiles="@(JarFiles)" 
        DestinationFiles="@(JarFiles->'$(WorkDirectory)\bin\lib\%(RecursiveDir)%(Filename)%(Extension)')" />
    <!-- Read the entire work directory after copying everything -->
    <CreateItem Include="$(WorkDirectory)\**\*" >
        <Output TaskParameter="Include" ItemName="Dependencies" />
    </CreateItem>
    <!-- Copy the contents of the work directory to our project (bin folder) -->
    <Copy SourceFiles="@(Dependencies)" 
        DestinationFiles="@(Dependencies->'%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
<PropertyGroup>
    <!-- Override the package properties so we can add our own files -->
    <CopyAllFilesToSingleFolderForPackageDependsOn>
        CustomCollectFiles;
        $(CopyAllFilesToSingleFolderForPackageDependsOn);
    </CopyAllFilesToSingleFolderForPackageDependsOn>
    <CopyAllFilesToSingleFolderForMsdeployDependsOn>
        CustomCollectFiles;
        $(CopyAllFilesToSingleFolderForPackageDependsOn);
    </CopyAllFilesToSingleFolderForMsdeployDependsOn>
</PropertyGroup>
<Target Name="CustomCollectFiles">
    <ItemGroup>
        <!-- Reference our files to add (Workdirectory) -->
        <_CustomFiles Include="$(WorkDirectory)\**\*" />
        <FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
            <DestinationRelativePath>%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
        </FilesForPackagingFromProject>
    </ItemGroup>
</Target>

Open the solution in Visual Studio. You'll notice that the bin folder will be populated by our files and folders automatically. So you can add the bin folder to your scm ignore files again!

After this you can use publishing from Visual Studio (This will create the same type of packages I refered to earlier), but we won't want that. We want a script to do this for us, so we can have our Continuous Integration server create it.

MSBuild

Now we need a MSBuild file to kick off our project and create a package. First we'll need an instruction file for msdeploy (We'll need it when creating a package). This file is called a publish profile.

Publish profile

In the Configuration Folder, create a file called publish-profile.xml and edit it. Add the following contents:

<?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 process
by 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">
  <PropertyGroup>
    <WebPublishMethod>MSDeploy</WebPublishMethod>
    <RemoteSitePhysicalPath />
    <SkipExtraFilesOnServer>False</SkipExtraFilesOnServer>
    <MSDeployPublishMethod>WMSVC</MSDeployPublishMethod>
    <EnableMSDeployBackup>True</EnableMSDeployBackup>
    <PrecompileBeforePublish>True</PrecompileBeforePublish>
    <EnableUpdateable>True</EnableUpdateable>
    <DebugSymbols>False</DebugSymbols>
    <WDPMergeOption>DonotMerge</WDPMergeOption>
  </PropertyGroup>
</Project>

MSBuild script

In the root folder of your project (Where your solution is at), create a file called [ProjectName].msbuild and add the following:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Package" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
    <PropertyGroup>
        <!-- Main settings -->
        <Project>DD4T.MVC.Sample</Project>
        <PackageProject>$(Project)</PackageProject>
        <Configuration>Release</Configuration>
        <VisualStudioVersion>12.0</VisualStudioVersion>

        <!-- Root directory -->
        <BaseDir>$(MSBuildProjectDirectory)</BaseDir>

        <!-- Solution and Project settings -->
        <SolutionFile>$(BaseDir)\$(Project).sln</SolutionFile>
        <ProjectFile>$(BaseDir)\$(PackageProject)\$(PackageProject).csproj</ProjectFile>

        <!-- Directory references -->
        <OutputDirectory>$(BaseDir)\Deployment</OutputDirectory>
        <WorkDirectory>$(BaseDir)\Work</WorkDirectory>
        <PackageDirectory>$(OutputDirectory)\Package</PackageDirectory>

        <ConfigurationDirectory>$(BaseDir)\Configuration</ConfigurationDirectory>

        <!-- Publish Profile -->
        <PubXMLFile>$(ConfigurationDirectory)\publish-profile.pubxml</PubXMLFile>

    </PropertyGroup>
    <ItemGroup>
        <DeploymentFolders Include="$(OutputDirectory)"/>
        <DeploymentFolders Include="$(WorkDirectory)"/>
        <DeploymentFolders Include="$(PackageDirectory)"/>
    </ItemGroup>
    <Target Name="Clean">
        <RemoveDir Directories="@(DeploymentFolders)" />
        <MSBuild Projects="$(SolutionFile)" ContinueOnError="false" Targets="Clean"/>
    </Target>
    <Target Name="PreCompile" DependsOnTargets="Clean">
        <MakeDir Directories="@(DeploymentFolders)"/>
    </Target>
    <Target Name="Compile" DependsOnTargets="PreCompile">
        <MSBuild Projects="$(ProjectFile)" ContinueOnError="false" Properties="Configuration=$(Configuration);PublishProfile=$(PubXMLFile);VisualStudioVersion=$(VisualStudioVersion)" Targets="Package">
            <Output ItemName="OutputFiles" TaskParameter="TargetOutputs"/>
        </MSBuild>
    </Target>
    <Target Name="CopyFiles">
        <CreateProperty Value="$(BaseDir)\$(PackageProject)\obj\$(Configuration)\Package">
            <Output TaskParameter="Value" PropertyName="PackageLocation" />
        </CreateProperty>
        <CreateProperty Value="$(PackageLocation)\$(Project).zip">
            <Output TaskParameter="Value" PropertyName="MSDeployPackage" />
        </CreateProperty>
        <CreateItem Include="$(MSDeployPackage)">
            <Output TaskParameter="Include" ItemName="MSDeployPackageFiles" />
        </CreateItem>

        <Copy SourceFiles="@(MSDeployPackageFiles)" 
            DestinationFiles="@(MSDeployPackageFiles->'$(PackageDirectory)\%(RecursiveDir)MSDeploy-%(Filename)%(Extension)')" />
    </Target>
    <Target Name="Package">
        <CallTarget Targets="Compile" />
        <CallTarget Targets="CopyFiles" />
    </Target>
</Project>

The default target is set to Package and default configuration is set to Release, so open a command window and run "MSBuild [ProjectName].msbuild" or hit CTRL+b in Sublime.

A new directory called Package has been created for you, so you can refer to this directory quickly. The Work and the Package Directory should be added to your scm ignore file.

So now you have a package that can be deployed with MSDeploy. I will get to actually deploying in my next post.

This might be an idea for the DD4T Visual Studio Template, what do you think? Give a shout in the comments below.

Source files on GitHub: DD4T.Package.Sample