Question

I'm working on migrating a Windows Service VDPROJ to WiX.

I was able to use HEAT to harvest output from my Windows Service project into a fragment. Currently, in order for my custom actions to work properly, I manually change some of the generated GUIDs from the Heat-generated file into known strings that are referenced in the main Product.wxs.

I need to do this programmatically on every build instead of relying on manual intervention since I need to integrate the WiX project into our continuous build server.

From what I could research, I can use an XSLT transform on the output of HEAT to achieve what I need, but I'm having a hard time making my XSLT transform work.

Here is a section of the generated fragment without using the XSLT transform

Fragments\Windows.Service.Content.wxs

<?xml version="1.0" encoding="utf-8"?>
  <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  [...]
    <Fragment>
      <ComponentGroup Id="Windows.Service.Binaries">
        <ComponentRef Id="ComponentIdINeedToReplace" />
        [...]
      </ComponentGroup>
    </Fragment>
    [...]
    <Fragment>
      <ComponentGroup Id="CG.WinSvcContent">
        <Component Id="ComponentIdINeedToReplace" Directory="TARGETDIR" Guid="{SOMEGUID}">
          <File Id="FileIdINeedToReplace" Source="$(var.Windows.Service.TargetDir)\Windows.Service.exe" />
        </Component>
        [...]
      </ComponentGroup>
    </Fragment>
    [...]
  </Wix>

I modified the HEAT prebuild command to:

"$(WIX)bin\heat.exe" project "$(ProjectDir)\..\Windows.Service\Windows.Service.csproj" -gg -pog Binaries -pog Symbols -pog Content -cg CG.WinSvcContent -directoryid "TARGETDIR" -t "$(ProjectDir)Resources\XsltTransform.xslt" -out "$(ProjectDir)Fragments\Windows.Service.Content.wxs"

and wrote the following XSLT to achieve two things:

  • Replace all occurrences of "ComponentIdINeedToReplace" to a known string (there's two)
  • Replace single ocurrence of "FileIdINeedToReplace" to a known string

Resources\XsltTransform.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:variable
     name="vIdToReplace"
     select="//ComponentGroup[@Id='CG.WinSvcContent']/Component/File[contains(@Source,'Windows.Service.exe') and not(contains(@Source,'config'))]/../@Id" />

  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="node[@Id=vIdToReplace]">
    <xsl:copy-of select="@*[name()!='Id']"/>
    <xsl:attribute name="Id">C_Windows_Service_exe</xsl:attribute>
  </xsl:template>

  <xsl:template 
     match="//ComponentGroup[@Id='CG.WinSvcContent']/Component/File[contains(@Source,'Windows.Service.exe') and not(contains(@Source,'config'))]">
    <xsl:copy-of select="@*[name()!='Id']"/>
    <xsl:attribute name="Id">Windows_Service_exe</xsl:attribute>
  </xsl:template>
</xsl:stylesheet>

How can I modify my XSLT to achieve what I need?

Was it helpful?

Solution

You've got a number of problems with your XSLT, I am afraid. The first one is to do with namespaces. In your wix XML file, all the elements are within the namespace "http://schemas.microsoft.com/wix/2006/wi", but there is no mention of that namespace in the XSLT, meaning where you try to match a named element, it will only match ones which are in NO namespace, which is not the case for your wix file.

What you need to add a corresponding namespace declaration to you XSLT, like so

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wi="http://schemas.microsoft.com/wix/2006/wi">

Then, where you reference named elements, prefix them with this declared prefix. For example, your template matching File would look like this

<xsl:template match="wi:ComponentGroup[@Id='CG.WinSvcContent']/wi:Component/wi:File[contains(@Source,'Windows.Service.exe') and not(contains(@Source,'config'))]">

Now, you've also got a problem within this template, because the first thing you do is output an attribute, but you've not actually copied the File element at this point, so you will probably get an error as it won't have anything to add the attributes to. The template, therefore, probably needs to look like this

<xsl:template match="wi:ComponentGroup[@Id='CG.WinSvcContent']/wi:Component/wi:File[contains(@Source,'Windows.Service.exe') and not(contains(@Source,'config'))]">
   <xsl:copy>
      <xsl:attribute name="Id">Windows_Service_exe</xsl:attribute>
      <xsl:copy-of select="@*[name()!='Id']"/>
      <xsl:apply-templates />
   </xsl:copy>
</xsl:template>

(I've added in an xsl:apply-templates here, but it is probably unnecessary in this case, if File elements don't ever have child elements)

You've also got problems with the previous template too

<xsl:template match="node[@Id=vIdToReplace]">

I am guessing you meant to use "node()" and not "node" here, as "node" itself would literally look for an element called "node" of which there are none. But the main problem is where you compare @Id with vIdToReplace. In this case, it is looking for an element called vIdToReplace, when you really want to compare it with your variable. The correct syntax is $vIdToReplace

<xsl:template match="node()[@Id=$vIdToReplace]">

But wait! If you are using XSLT 1.0, you will get an error. You cannot use variables in template matching like this in XSLT 1.0. It only works in XSLT 2.0. What you could do though, is simply paste in your long expression into the template match:

<xsl:template match="node()[@Id=//wi:ComponentGroup[@Id='CG.WinSvcContent']/wi:Component[wi:File[contains(@Source,'Windows.Service.exe') and not(contains(@Source,'config'))]]/@Id]">

But this looks rather ungainly. Alternatively, you could define a key to look up the Component element containing the id you wish to replace:

<xsl:key name="vIdToReplace"
         match="wi:ComponentGroup[@Id='CG.WinSvcContent']/wi:Component[wi:File[contains(@Source,'Windows.Service.exe') and not(contains(@Source,'config'))]]"
         use="@Id"/>

Then you can use this key in the template match, to check if the Component elements exists for the current @Id

<xsl:template match="node()[key('vIdToReplace', @Id)]">

Here is the full XSLT in this case

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wi="http://schemas.microsoft.com/wix/2006/wi">
  <xsl:key name="vIdToReplace"
  match="wi:ComponentGroup[@Id='CG.WinSvcContent']/wi:Component[wi:File[contains(@Source,'Windows.Service.exe') and not(contains(@Source,'config'))]]"
  use="@Id"/>

  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="node()[key('vIdToReplace', @Id)]">
    <xsl:copy>
      <xsl:attribute name="Id">C_Windows_Service_exe</xsl:attribute>
      <xsl:copy-of select="@*[name()!='Id']"/>
      <xsl:apply-templates />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="wi:ComponentGroup[@Id='CG.WinSvcContent']/wi:Component/wi:File[contains(@Source,'Windows.Service.exe') and not(contains(@Source,'config'))]">
     <xsl:copy>
        <xsl:attribute name="Id">Windows_Service_exe</xsl:attribute>
        <xsl:copy-of select="@*[name()!='Id']"/>
        <xsl:apply-templates />
     </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

When applied to your XML, the following is output

  <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  [...]
    <Fragment>
      <ComponentGroup Id="Windows.Service.Binaries">
        <ComponentRef Id="C_Windows_Service_exe"/>
        [...]
      </ComponentGroup>
    </Fragment>
    [...]
    <Fragment>
      <ComponentGroup Id="CG.WinSvcContent">
        <Component Id="C_Windows_Service_exe" Directory="TARGETDIR" Guid="{SOMEGUID}">
          <File Id="Windows_Service_exe" Source="$(var.Windows.Service.TargetDir)\Windows.Service.exe"/>
        </Component>
        [...]
      </ComponentGroup>
    </Fragment>
    [...]
  </Wix>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top