MSBuild task to generate MSI Transform files from XML

March 11, 2008 at 10:05 pm | In TFSBuild.com | 5 Comments

In my previous post I described how you can use MSI Transform (*.mst) files to separate environment-specific configuration settings from a generic MSI package.

Although this provides a level of flexibility in your configuration, there are two down-sides of Transform files:

  1. They’re a binary format
  2. You can’t “edit” or “view” a transform file. You have to apply it or re-create it to see what changes it includes.

Fortunately we can use the Microsoft Windows Installer Object Library (c:\windows\system32\msi.dll) to open MSI “databases” and create transform files.

Credits go again to Alex Shevchuk - From MSI to WiX - Part 7 - Customising installation using Transforms for showing us how to achieve this with VbScript. Essentially all I’ve done is taken Alex’s example and using Interop.WindowsInstaller.dll I’ve implemented an MSBuild task.

The <GenerateMsiTransform> MSBuild Task

Download the source code & example transforms.xml here (~7Kb Zipped VS2008 Solution)

The <GenerateMsiTransform> MSBuild task generates MSI Transform (*.mst) files with updated configuration properties for a specified MSI package from an XML configuration file.

This removes the need to manually create transforms using the ‘Orca’ MSI editing tool.

Since the generation of transforms can now be automated, it also means that environment configurations can be stored & versioned under source control.

How it works

GenerateMsiTransform MSBuild Task Overview

Because the WindowsInstaller is an unmanaged COM object and there’s no functionality to “close” a database, the file handle remains open. To work around this we have to create temporary copies of the original MSI file. The workflow goes something like this:

  • Read each <transform> element from the ConfigFile
  • For each transform:
    • Make the changes to a copy of the original MSI file
    • Copy the MsiFile to a temporary file
    • Update the properties / values with the values from the current <transform> element
    • Save the changes
    • Copy the resultant MSI to a new temporary file
  • Diff the changed MSI with the original MSI
    • Open original MSI
    • Open changed MSI
    • Generate a transform file by diff’ing the two

Using the task

Although the task could be used as a stand-alone task invoked by msbuild.exe, the ideal place for it is part of a Team Build script.

By overriding the “AfterCompile” target in TFSBuild.proj, you can create the Transform files just after the installer is built & it will get copied up to the Team Build drop directory.

<UsingTask TaskName=”GenerateMsiTransform” AssemblyFile=”$(MSBuildExtensionsPath)\GenerateMsiTransformTask.dll” />

<Target Name=”AfterCompile”>

  <PropertyGroup>

    <MsiFile>$(Outdir)\Project.Deployment.msi</MsiFile>

    <TransformConfigFile>$(SolutionRoot)\transforms.xml</TransformConfigFile>

  </PropertyGroup>

  <GenerateMsiTransform MsiFile=”$(MsiFile)” ConfigFile=”$(TransformConfigFile)”>

    <Output ItemName=”TransformFiles” TaskParameter=”Transforms” />

    <Output ItemName=”TempFiles” TaskParameter=”TemporaryFiles” />

  </GenerateMsiTransform>

  <Message Text=”Created transform files: @(TransformFiles)” />

  <Message Text=”Created temporary files: @(TempFiles)” />

</Target>

Available Properties

Property Description
MsiFile The base MSI installer that you want to generate the transform for
ConfigFile The configuration file that contains the properties to add to the transform
Transforms (Output) An output property that includes the path to each of the transform files created
TemporaryFiles (Output)

An output property that includes the path to each of the temporary MSI files created as part of the generation process.

Transform Configuration File (transforms.xml)

The Transform configuration file is designed to be as simple as possible. Each <property /> element specifies a key name & value. Here’s an example:

<?xml version=”1.0″ encoding=”utf-8″?>

<transforms>

  <transform name=”Development”>

    <property name=”CONFIG_CONNSTRING” value=”Data Source=DEVSQLSERVER;Initial Catalog=HelloWorldDevelopment;Integrated Security=True” />

    <property name=”CONFIG_DEBUGENABLED” value=”True” />

  </transform>

  <transform name=”Test”>

    <property name=”CONFIG_CONNSTRING” value=”Data Source=TESTSQLSERVER;Initial Catalog=HelloWorldTest;Integrated Security=True” />

    <property name=”CONFIG_DEBUGENABLED” value=”True” />

  </transform>

</transforms>

I hope this helps somebody out there - please leave a comment if you find it useful or extend it in interesting ways. For more interesting Team Build recipes like this, be sure to have a look at TFSBuild.com

Customising your WiX MSI with Transform files

March 11, 2008 at 9:23 pm | In TFSBuild.com | 1 Comment

In the different applications development areas that I’ve worked in, the typical process for getting a product installed is something like this:

Typical workflow of an MSI

  1. Developers are responsible for building the installation package (a WiX MSI is preferred)
  2. Then it’s left up to IT to update the config files
  3. And IT deploy the installer to a server or end-users.

Step #2 is where things usually come undone. Some common mistakes I’ve seen people make are:

  • There’s no configuration screens in the installer to specify configuration.
  • Sometimes the IT guys rip apart the package and re-package it.
  • Sometimes the installer doesn’t deal with configuration at all - and the IT guys are left hand-editing XML configuration files on servers.

One of the really nice ways you can avoid these mistakes is to make use of Windows Installer Transform Files for environment-specific configuration settings.

A transform is a collection of changes applied to an installation. By applying a transform to a base installation package, the installer can add or replace data in the installation database.

Credits go to Alex Shevch’s blog post - From MSI to WiX, Part 7 Customising installation using Transforms for describing this technique in detail.

A typical output from the developers now looks like this:

MSI package and it's related Transforms

Step 1 - Parameterising your WiX script

Within your WiX script, you can refer to properties by placing square-brackets around them. e.g. [CONFIG_CONNECTIONSTRING]

<util:XmlFile Id=”SetConnString” Action=”setValue” ElementPath=”//appSettings/add[\[]@key=’MyConnectionString’[\]]/@value” Value=”[CONFIG_CONNECTIONSTRING]” File=”[INSTALLDIR]ConsoleApp.exe.config” />

Now that a property is defined in the MSI, we can set it from the command line like so:

msiexec.exe /i MyInstaller.msi CONFIG_CONNECTIONSTRING=”Data Source=(LOCAL);etc…”

By default, Windows Installer doesn’t do any verification of properties. If a property isn’t set, then it will just replace it with nothing when it comes across it. If you want to ensure that all your required properties are set, you can use a launch condition. e.g.

<Condition Message=”CONFIG_CONNECTIONSTRING variable is not set. Try setting the property from the command line or using a transform.”>Installed OR CONFIG_CONNECTIONSTRING</Condition>

Step 2 - Creating a transform file

If you were to create a transform manually, the tool you would use is ORCA. (Not to be confused with “Orcas” - the code name for Visual Studio 2008). Orca is distributed in the (large >1GB) Platform SDK download. Fortunately Aaron Stebner has made just the ORCA installer available through is blog.

Aaron Stebner’s blog - Download Orca install package (direct)

You can quickly access Orca by right-clicking any MSI file and selecting “Edit with Orca” from the context menu:

Edit with Orca context menu

Once you’ve opened the MSI editing utility, select the “Property” table and have a look at the different properties you can set. If you’ve configured your WiX script correctly, you should be able to see the properties that you made available.

Editing MSI with Orca

Step 2a - Manually creating a transform

  • From the menu, select Transform | New Transform
  • Make changes to any properties with your environment-specific configuration
  • select Transform | Generate Transform…
  • Type the filename of the *.mst file you want to save your differences as
  • Repeat the process for each transform/environment that you need to

Creating a transform in Orca

Now when you run your installation, you specify the transform file as a parameter:

msiexec.exe /i MyInstaller.msi TRANSFORMS=Development.mst

Conclusion

This is an excellent way to reuse a single MSI as you promote it through your dev/test/qa/prod environments, without having to know the values up-front and embed them in the MSI.

In my next post I show you how I’ve extended this technique to automatically generate transform files based on an XML configuration file using the Windows Installer COM Automation object and an MSBuild task.

Building a Release Notes Text File with Team Build

February 19, 2008 at 10:48 pm | In TFSBuild.com, Team Foundation Server | 4 Comments

The following post describes a Team Build target that generates a text file (e.g. ReleaseNotes.txt) from Team Build’s build log and places it with the build output.

This is useful if you want to distribute your product to somebody who won’t have access to your TFS server to view the build log history.

This is an example of the output that is generated:

Screenshot of Notepad showing contents of ReleaseNotes.txt 

How it works

Team Build compiles the solution and after it performs the GetChangesetsAndUpdateWorkItems task, we download the build log (as XML) using the Build.aspx web page that TFS provides.

The build log is an XML file that contains all the associated work items and changesets that have been included in the build.

We perform an XSL transform on this file to generate the text file output that we require.

Flow chart of creating the release notes file

Setup: TFSBuild.proj

To actually get Team Build to generate the release notes, we need override the AfterGetChangesetsAndUpdateWorkItems target and call our new target.

In source control, checkout the TFSBuild.proj file for your build definition and insert the following snippet at the end, just before the </Project> tag.

<Target Name="AfterGetChangesetsAndUpdateWorkItems">
  <CallTarget Targets="GenerateReleaseNotes" />
</Target>
 
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<!-- Download the build log from the Build.aspx web app and tranform to ReleaseNotes.txt-->
<Target Name="GenerateReleaseNotes">
  <PropertyGroup>
    <BuildDownloadBaseUri>$(TeamFoundationServerUrl)Build/Build.aspx?artifactMoniker=</BuildDownloadBaseUri>
    <BuildLogFile>buildlog.xml</BuildLogFile>
    <ReleaseNotesXslFile>$(MSBuildExtensionsPath)\releasenotes.xsl</ReleaseNotesXslFile>
    <ReleaseNotesOutputFile>$(OutDir)ReleaseNotes.txt</ReleaseNotesOutputFile>
  </PropertyGroup>
 
  <!-- Extract the artifact ID from the BuildUri for passing to Build.aspx -->
  <RegexReplace Input="$(BuildUri)" Expression="vstfs:///Build/Build/" Replacement="" Count="1">
   <Output ItemName="BuildLogUriId" TaskParameter="Output" />
  </RegexReplace>
 
  <WebDownload
    FileUri="$(BuildDownloadBaseUri)@(BuildLogUriId)"
    FileName="$(BuildLogFile)"
    UseDefaultCredentials="True" />
 
  <Exec Command="msxsl.exe &quot;$(BuildLogFile)&quot; &quot;$(ReleaseNotesXslFile)&quot; -o &quot;$(ReleaseNotesOutputFile)&quot;" />
</Target>

Configuration

Property Description Default
BuildDownloadBaseUri The URI to download the build log file from. $(TeamFoundationServerUrl)Build/Build.aspx?artifactMoniker=
BuildLogFile Path to output the downloaded build log to. buildlog.xml
ReleaseNotesXslFile Path to the XSL transform used to generate the release notes text file. $(MSBuildExtensionsPath)\releasenotes.xsl
ReleaseNotesOutputFile The output of the XSL transform $(OutDir)ReleaseNotes.txt

Build Server Setup

The following pre-requisite software is required for the build server:

More Information

Building Sandcastle Documentation with Team Build

February 19, 2008 at 10:24 pm | In TFSBuild.com, Team Foundation Server | 8 Comments

This post outlines how to configure Team Build, Sandcastle and the Sandcastle Help File Builder (SHFB) to generate MSDN-style documentation for assemblies.

Sandcastle produces accurate, MSDN style, comprehensive documentation by reflecting over the source assemblies and optionally integrating XML Documentation Comments. It is command line based and has no GUI front-end, project management features, or an automated build process like those that you can find in NDoc.

Sandcastle Help File Builder is a frontend for Sandcastle that provides graphical and command line based tools to build a help file in an automated fashion.

How it works

The usual way to configure SHFB is to open the GUI and create a SHFB project file from your solution file with your desired configuration. Then you’re supposed to check this file into source control and maintain it each time you add a new assembly.

This post outlines a possibly simpler approach - it uses a common SHFB project template file and passes the assemblies that Team Build generates as arguments to the SandcastleBuilderConsole.exe program. This means it automatically includes all assemblies that have been built.

Once the documentation has been built, it is copied to the Team Build drop location along with the binaries.

Sandcastle Team Build Target

Setup: TFSBuild.proj

To actually get Team Build to generate the documentation, we need override the GenerateDocumentation target and insert our new target into the project.

In source control, checkout the TFSBuild.proj file for your build definition and insert the following snippet at the end, just before the </Project> tag.

<Import  Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>


<Target Name="GenerateDocumentation">

  <PropertyGroup>

    <SandcastleBuidlerPath>C:\Program Files\EWSoftware\Sandcastle Help File Builder\SandcastleBuilderConsole.exe</SandcastleBuidlerPath>

    <SandcastleBuilderProjectTemplateFile>$(MSBuildExtensionsPath)\template.shfb</SandcastleBuilderProjectTemplateFile>

    <SandcastleBuilderProjectFile>$(MSBuildProjectDirectory)\project.shfb</SandcastleBuilderProjectFile>

    <SandcastleBuilderArguments>-assembly=&quot;$(OutDir)*.dll&quot; -outputpath=&quot;$(OutDir).&quot;</SandcastleBuilderArguments>

    <!-- -dependency=&quot;$(SolutionRoot)\References\*.dll&quot; -->

    <!-- HACK: For some reason the -outputpath argument requires a '.' on the end,

     otherwise you get BUILD FAILED: Illegal characters in path. -->

  </PropertyGroup>

 

  <Error Text="Sandcastle Help File Builder is not found at $(SandcastleBuidlerPath)." Condition="!Exists('$(SandcastleBuidlerPath)')"  />

  <Error Text="Sandcastle Help File Builder project template is not found at $(SandcastleBuilderProjectTemplateFile)."

         Condition="!Exists('$(SandcastleBuilderProjectTemplateFile)')" />

 

  <Copy SourceFiles="$(SandcastleBuilderProjectTemplateFile)" DestinationFiles="$(SandcastleBuilderProjectFile)" />

 

  <XmlUpdate XPath="/project/HelpTitle" XmlFileName="$(SandcastleBuilderProjectFile)" Value="Documentation for $(BuildDefinition)" />

  <XmlUpdate XPath="/project/HtmlHelpName" XmlFileName="$(SandcastleBuilderProjectFile)" Value="$(BuildDefinition)" />

  <XmlUpdate XPath="/project/FooterText" XmlFileName="$(SandcastleBuilderProjectFile)" Value="Build Number: $(BuildNumber)" />

 

  <Exec Command="&quot;$(SandcastleBuidlerPath)&quot; &quot;$(SandcastleBuilderProjectFile)&quot; $(SandcastleBuilderArguments)" />

 

</Target>

Configuration

Property Description Default
SandcastleBuidlerPath Path to SandcastleBuilderConsole.exe C:\Program Files\EWSoftware\Sandcastle Help File Builder\SandcastleBuilderConsole.exe
SandcastleBuilderProjectTemplateFile Path to the template SHFB project file created via the GUI $(MSBuildExtensionsPath)\template.shfb
SandcastleBuilderProjectFile Path to copy the template SHFB project file to, before modifying it for the current build $(MSBuildProjectDirectory)\project.shfb
SandcastleBuilderArguments Arguments to pass to SandcastleBuilderConsole.exe -assembly=&quot;$(OutDir)*.dll&quot; -outputpath=&quot;$(OutDir).&quot;

Build Server Setup

The following pre-requisite software is required for the build server:

Blocked CHM Help Files from a Network Share

Because the drop location is a network share, the viewing of CHM files is often blocked. This is because network shares fall into the “Intranet” security zone, which isn’t trusted.

There are two options for getting around this:

  1. Copy the documentation file to your local machine, right-click, select properties, select Unblock.
  2. Or disable blocking for the Intranet zone by creating the following registry key. (More information here)
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\HTMLHelp\1.x\ItssRestrictions]
"MaxAllowedZone"=dword:00000001

TODO

  • It may be necessary to cater for References or Dependent assemblies. This would be possibly via another command line argument and a <CreateItem> task to search $(SolutionRoot)\References\**\*.dll
  • Namespace documentation is usually configured via the SHFB project file, I haven’t covered this in this implementation.

More Information

« Previous PageNext Page »

Blog at WordPress.com. | Theme: Pool by Borja Fernandez.
Entries and comments feeds.