Friday 18 January 2008

Altering ASP.NET web.config during WIX MSI installation

Here's an example of how to amend a web.config file during an ASP.NET website setup via a wix MSI. I needed to do this for a project recently, and couldn't find any examples at the time. In wix3 you can use the XmlConfig element from the wixUtilExtension.dll, but the syntax varies quite a lot depending on exactly what you're trying to achieve, so other examples I found for other kinds of config file changes didn't help as much as I'd hoped. Anyway, here's the example.

The purpose of this change is to:

  • Make sure debug file generation is switched off after installation (it may have been switched on during development).

  • Add a reference to an assembly in the global assembly cache (GAC) (and the version number changes each deployment as this is a major upgrade).

The hardest part is the fact that I want to add a new element to a list (an
<add> element to the existing <assemblies> list, and then set an attribute on the newly added element. That involves two steps in wix one to add the element, the other to set the attribute on it), and the second step needs to work against the exact element just added. Sometimes that's easy if the element you just added has a unique key. I guess if I knew more xpath I'd be able to work out a query that would identify the new element as it doesn't already have an attribute. However, there seemed to be a new feature in wix3 where the XmlConfig element can be nested within another one, and also where instead of an xpath expression for the element you're changing an attribute on, you can use the Id of the XmlConfig node for the parenent element. (I may be wrong, but this only seemed to work if you used the nesting, i.e. you can't have all your XmlConfig nodes at the same level and expect one to be able to reference another).

Here's how the web.config file is when it gets built into the MSI (and how it would get deployed if we didn't amend it) (irrelevent bits skipped):

<configuration>
<system.web>
<compilation debug="true">
<assemblies>
<add
assembly="System.Core, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>

<add
assembly="System.Web.Extensions, Version=3.6.0.0, Culture=neutral,
PublicKeyToken=31BF3856AD364E35"/>

<add
assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>

<add
assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>

<add
assembly="System.Data.Linq, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>

</assemblies>
</compilation>
</system.web>
</configuration>

And here's how we really want it to be after deployment:

<configuration>
<system.web>
<compilation debug="false">
<assemblies>
<add
assembly="System.Core, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>

<add
assembly="System.Web.Extensions, Version=3.6.0.0, Culture=neutral,
PublicKeyToken=31BF3856AD364E35"/>

<add
assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>

<add
assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>

<add
assembly="System.Data.Linq, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>

<add
assembly="MyGacAssembly, Version=1.0.1003.0, Culture=neutral,
PublicKeyToken=f00a07e3b937d388"/>

</assemblies>
</compilation>
</system.web>
</configuration>

The relevent wix XML to do the job is as follows:

<Component Id="MyGacAssembly.dll" Guid="...">
<File Name="MyGacAssembly.dll"
Source="$(var.MyAssemblyProject.TargetDir)" Assembly=".net" KeyPath="yes" />

</ComponentId>
<Component Id="WebConfigFile" Guid="..." KeyPath="yes">
<File Name="web.config"
Source="$(var.MyWebProject.ProjectDir)" />

<util:XmlConfig Sequence="1"
Id="SwitchOffDebug" File="[WebFolder]\web.config" Action="create" On="install"
Node="value" ElementPath="/configuration/system.web/compilation" Name="debug"
Value="false" />

<util:XmlConfig Sequence="2"
Id="NewAssemblyElement" File="[WebFolder]\web.config" Action="create"
On="install" Node="element"
ElementPath="/configuration/system.web/compilation/assemblies" Name="add">

<util:XmlConfig Sequence="3"
Id="NewAssemblyAttribute" File="[WebFolder]\web.config"
ElementPath="NewAssemblyElement" Name="assembly"
Value="!(bind.assemblyFullName.MyGacAssembly.dll)" />

</util:XmlConfig>
</Component>

Other notes...

  • The util: prefix comes from adding the wixUtilExtension.dll as a reference and adding its namespace to the wix file.

  • The !(bind.assemblyFullName...) syntax tells wix to replace this pattern with the fully qualified name of the assembly referred to by its file ID (name). You can also use things like !(bind.assemblyVersion...) etc if you need to get other info. But you can only apply this to files which have their Assembly=".net" attribute set. Normally that means they're assemblies that go into the GAC, but you can prevent them being GAC'ed by using the AssemblyApplication parameter on the File node to refer to the directory where the assembly will reside instead.

Thursday 17 January 2008

Installing a powershell cmdlet using wix

I recently tried to create a wix MSI setup project that included registering a custom powershell snap-in as part of the installation process. I ran into a few problems that weren't covered by the existing documentation or other blog posts at the time, so thought it would be useful to document the gotcha's I found and their solutions.

I've been using the WixPSExtension to WIX3 from within Visual Studio 2008 to create a setup for a project that includes a custom Windows PowerShell snap-in. (See Heath Stewart's blog.) I was using the latest version of wix at the time: 3.0.3711.0 (11 Jan 2008). That version comes with an extension DLL called WixPSExtension.DLL to help register powershell snap-ins without having to run INSTALLUTIL against them. My understanding is that the more of your installation that can be done directly within wix without having to call external programs at install time the better.

Registering a powershell snap-in (for powershell 1.0 at least) involves writing some registry entries on the machine under HKLM\software\microsoft\powershell\1\[snap-in]. These are the keys that powershell looks for when you type get-pssnapin -registered and it lists the snap-ins that are available for you to add, via add-pssnapin [name] where name comes from the list above.

This is what happens when you build your own snap-in with a class in it derived from PSSnapIn and mark it as [RunInstaller(true)], then run installutil.exe [snapin.dll]. Doing it that way way was the only option when building an installer using the built-in setup project in visual studio. But that means the MSI file that gets built doesn't directly show the requirements to change the registry as part of the install / uninstall process, that info is encoded within a DLL that gets run via a custom action. In the past I've run into problems with this where the DLL used by the custom action gets loaded by MSIEXEC.EXE and can't be re-loaded, even if the DLL should really be a different version between uninstalling the application and re-installing it (in the case of upgrading an existing installation to a newer version). So my gut feel is that having the MSI explicitly say what is required directly will always be best. Wix allows you to do this but in a high-level way.

At the time, the only examples / documentation I could find suggested the syntax to use was something like this:

<Component Id="MyId" Guid="12345678-...">
<File Name="mycmdlet.dll" Source="$(var.MyProject.TargetDir)" KeyPath="yes">
<ps:SnapIn id="MySnapIn" Description="This is my snap-in." Vendor="Company UK" AssemblyName="mycmdlet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f00a07e3b937d388"/>
</File>
</Component>

If you build this it tells you that AssemblyName is no longer required and is automatically worked out. You remove AssemblyName and find that after installing the snap-in through the MSI, the registry has been updated but you have the following two values that weren't expected:

AssemblyName = !(bind.assemblyFullName.mycmdlets.dll)
Version = !(bind.assemblyVersion.mycmdlets.dll)

When really they should have been a fully qualified assembly name and an assembly version respectively. If you try to register the snap-in through poswershell, you'll see it listed (get-pssnapin -registered) but if you try to register is (add-pssnapin) you get an error that the assembly doesn't have the right strong name. The real problem is the two registry entries above.

The key to fixing this was to explicity say that the file that's being installed (the snap-in) is actually a .NET assembly. That's what triggers some code in wix to parse the !(bind... string and actually convert it into its actual value. So the example above becomes:

<Component Id="MyId" Guid="12345678-...">
<File Name="mycmdlet.dll" Source="$(var.MyProject.TargetDir)" KeyPath="yes" Assembly="'.net">
<ps:SnapIn Id="MySnapIn" Description="This is my snap-in." Vendor="Company UK"/></File>
</Component>

I hadn't realsied the significance of specifying that a file being installed was actually an assembly. But it makes sense really that whenever you are deploying a file that's really an executable assembly, wix should know so it can find out version information from it etc.

Other notes...

- In the examples above, the ps namespace comes from using xmlns:ps="http://schemas.microsoft.com/wix/PSExtension"

- You may need to manually copy the ps.xsd schema file from where it gets installed when you install wix3 to visual studio's schema folder, in order for VS2008 to give you intelli-sense etc (some of the extensions do have their schemas copied, other don't).

- You need to add a reference to the WixPSExtension.DLL from you wix project, so it gets passed to the candle.exe & light.exe.

- I'm using a visual studio solution to contain both the snap-in project and the installer project, and have the installer project reference the snap-in project using a project reference. Hence the use of the $(var.MyProject.TargetDir) placeholders etc.