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.

1 comment:

Unknown said...

Can you please let me know how is the PSVersion field populated when we execute get-pssnapins command?