2

(sorry this may be a bit long)

I have a WXS file that was generated by WiX's heat utility. I'm trying to modify it with an (existing) exclusions.xslt file to automatically exclude certain components based on the contents of another XML file (I'll call this parts.xml). The xslt file is currently used to remove some components/files/directories from the installer, but for a relatively static list.

The WXS file I'm transforming has File elements, all of which have a "Source" attribute, which is what the criteria is applied to for removing elements.

My goal is to read in the parts.xml file from my XSLT stylesheet and use that to exclude some elements from my Wix installer. The way we currently exclude elements is:

We first copy every element (the entire source xml) over. Like so:

<!-- Copy all attributes and elements to the output. -->
<xsl:template match="@*|*">
    <xsl:copy>
        <xsl:apply-templates select="@*" />
        <xsl:apply-templates select="*" />
    </xsl:copy>
</xsl:template>

then we use xsl:key instructions to mark certain items, like so:

<xsl:key name="removals" match="wix:Component[wix:File[contains(@Source, ')\fileToBeRemoved1.txt')]]" use="@Id" />
<xsl:key name="removals" match="wix:Component[wix:File[contains(@Source, ')\fileToBeRemoved2.exe')]]" use="@Id" />

Then we remove them with:

<xsl:template match="wix:Component[key('removals', @Id)]" />
<xsl:template match="wix:Directory[key('removals', @Id)]" />
<xsl:template match="wix:ComponentRef[key('removals', @Id)]" />

I think I've gotten to the point where I was able to read the parts.xml file into the stylesheet with:

<!-- Adapted from https://stackoverflow.com/a/30153713/5605122 and http://geekswithblogs.net/Erik/archive/2008/04/01/120915.aspx-->
<xsl:param name="srcroot" />
<xsl:variable name="filename">
    <xsl:call-template name="string-replace-all">
        <xsl:with-param name="text" select="concat($srcroot, 'foo/parts.xml')" />
        <xsl:with-param name="replace" select="'\'" />
        <xsl:with-param name="by" select="'/'" />
    </xsl:call-template>
</xsl:variable>

<xsl:variable name="partsfile" select="document(concat('file://', $filename))" />

<xsl:variable name="myOutputFiles">
    <xsl:call-template name="str:tokenize">
        <xsl:with-param name="string" select="normalize-space($partsfile/xsi:Apple/xsi:Banana[@Name = 'my_sdk']/xsi:Carrot/xsi:Fig[@Directory = 'OutputFiles'])"/> 
    </xsl:call-template>
</xsl:variable>

I got the str:tokenize from here. I imagine there are issues with the way I set myOutputFiles, especially with namespace issues, but I'm not entirely sure how to properly do this. I've added the xsi namespace to my top-level stylesheet node, but this didn't seem to produce any results when I printed out the value with:

<xsl:message terminate="yes">
    <xsl:text>INFO: </xsl:text>
    <xsl:text>&#xd;</xsl:text>
    <xsl:copy-of select="$myOutputFiles"/>
    <xsl:text>&#xd;</xsl:text>
</xsl:message>

My next step would be to somehow call an <xsl:key /> instruction over each of the tokens returned, so that the three template instructions I wrote above would remove the necessary items. But since <xsl:key/> has to be a top-level element in the stylesheet, I'm not sure how I could go about doing that.

Ultimately, once I get the tokens, I'd want to iterate over them and exclude them by marking them in a way similar to above:

<xsl:key name="removals" match="wix:Component[wix:File[contains(@Source, ')\{PUT STRING FROM TOKEN HERE}')]]" use="@Id" />

and then they'd be removed by the template instructions above. The ')\' prefix is important to prepend to the token string. This is how it knows to only remove components from the INSTALLDIRECTORY node, and not any subdirectories.

How can I go about accomplishing this task? Any help is much appreciated!

Edit:

I'm using MSXSL as my processor. I don't believe it can natively do str:tokenize, but I copied the template from here and included it at the bottom of my stylesheet to use. Other than it eliminating the <token> tags, it seems to work okay. If I call it with string="'file1 file2 file3'" and print it out the same way I printed above, it outputs "file1file2file3" to the console.

Here is some sample input XML before transformation:

<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <DirectoryRef Id="INSTALLDIRECTORY">
            <Component Id="cmp53284C0E6C6EC93D8D9DE8E3703596E4" Guid="*">
                <File Id="filBD973F0EAED6A0B34BE693B053368769" KeyPath="yes" Source="$(env.srcDir)\file1.txt" />
            </Component>
            <Component Id="cmp81302C0E6C6EC93D877778E270358FFE" Guid="*">
                <File Id="filAA273F0EAED6A0B34BE693B053A129EA" KeyPath="yes" Source="$(env.srcDir)\file2.exe" />
            </Component>
            <Component Id="cmp2A1630B8E0E70C310FC91CD5DADB5A43" Guid="*">
                <File Id="filF5C36F42ADA8B3DD927354B5AB666898" KeyPath="yes" Source="$(env.srcDir)\thisWillStay.txt" />
            </Component>
            <Component Id="cmpABC123AE6C6EC93D8D9DE8E370BEEF39" Guid="*">
                <File Id="fil72EA34F0EAED6A0B34BE693B05334421" KeyPath="yes" Source="$(env.srcDir)\Some.File.dll" />
            </Component>
            <Directory Id="dir2A411B40F7C80649B57155F53DD7D136" Name="ThisWillStay">
                <Component Id="cmpE7DF6F3C9BE17355EA10D49649C4957A" Guid="*">
                    <File Id="fil908CF12B8DD2E95A77D7611874EC3892" KeyPath="yes" Source="$(env.srcDir)\ThisWillStay\file1.txt" />
                </Component>
            </Directory>
        </DirectoryRef>
    </Fragment>
    <Fragment>
        <ComponentGroup Id="DeliveredFiles">
            <ComponentRef Id="cmp53284C0E6C6EC93D8D9DE8E3703596E4" />
            <ComponentRef Id="cmp81302C0E6C6EC93D877778E270358FFE" />
            <ComponentRef Id="cmp2A1630B8E0E70C310FC91CD5DADB5A43" />
            <ComponentRef Id="cmpABC123AE6C6EC93D8D9DE8E370BEEF39" />
            <ComponentRef Id="cmpE7DF6F3C9BE17355EA10D49649C4957A" />
        </ComponentGroup>
    </Fragment>
</Wix>

The referenced xml file (parts.xml) looks something like (the node-tree leading up to Fig is what's important, but there are other elements around it as well):

<?xml version="1.0" encoding="utf-8"?>
<Apple xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../foo/schema.xsd" >
    <!-- ... -->
    <Banana Name="my_sdk" SomeAttribute="${srcroot}some/path/to/file.txt">
        <Carrot>
            <Dill SomeOtherAttribute="SomeIdentifier"/>
            <Fig Directory="OutputFiles">
                foo/bar/file1.txt
                foo/bar/file2.exe
                foo/bar/Some.File.dll
                <!-- ... -->
            </Fig>
        </Carrot>
    </Banana>
    <!-- ... -->
</Apple>

The files specified in Fig relate to the components to be removed from the WXS file (their paths here are not important though).This reference file is based off of an XML Schema instance (the schema.xsd file), which looks something like:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="Apple">
        <!-- (various xs:elements defined for elements used in other parts file) -->
    </xs:element>
</xs:schema>

And the expected output XML:

<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <DirectoryRef Id="INSTALLDIRECTORY">
            <Component Id="cmp2A1630B8E0E70C310FC91CD5DADB5A43" Guid="*">
                <File Id="filF5C36F42ADA8B3DD927354B5AB666898" KeyPath="yes" Source="$(env.srcDir)\thisWillStay.txt" />
            </Component>
            <Directory Id="dir2A411B40F7C80649B57155F53DD7D136" Name="ThisWillStay">
                <Component Id="cmpE7DF6F3C9BE17355EA10D49649C4957A" Guid="*">
                    <File Id="fil908CF12B8DD2E95A77D7611874EC3892" KeyPath="yes" Source="$(env.srcDir)\ThisWillStay\file1.txt" />
                </Component>
            </Directory>
        </DirectoryRef>
    </Fragment>
    <Fragment>
        <ComponentGroup Id="DeliveredFiles">
            <ComponentRef Id="cmp2A1630B8E0E70C310FC91CD5DADB5A43" />
            <ComponentRef Id="cmpE7DF6F3C9BE17355EA10D49649C4957A" />
        </ComponentGroup>
    </Fragment>
</Wix>

The file1.txt component is now gone from the first fragment, and the corresponding ComponentRef is gone in the second fragment. (the paths don't matter for the reference file. It should use only the file names to exclude files only in the INSTALLDIRECTORY node, not any subdirectories of the source file. I figured I could just use a "substring-after-last" like this).

Edit2:

If it helps, here are some templates I've grabbed from various sources I planned to use:

<!--
Adapted from: https://stackoverflow.com/a/9079154/5605122
-->
<xsl:template name="substring-after-last">
    <xsl:param name="haystack" />
    <xsl:param name="needle" />
    <xsl:choose>
        <xsl:when test="contains($haystack, $needle)">
            <xsl:call-template name="substring-after-last">
                <xsl:with-param name="haystack"
                    select="substring-after($haystack, $needle)" />
                <xsl:with-param name="needle" select="$needle" />
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise><xsl:value-of select="$haystack" /></xsl:otherwise>
    </xsl:choose>
</xsl:template>

<!-- Tokenizing: http://exslt.org/str/functions/tokenize/index.html -->
<!-- Code from: http://exslt.org/str/functions/tokenize/str.tokenize.template.xsl -->
<xsl:template name="str:tokenize">
    <xsl:param name="string" select="''"/>
    <xsl:param name="delimiters" select="' '"/>
    <xsl:choose>
        <xsl:when test="not($string)"/>
        <xsl:when test="not($delimiters)">
            <xsl:call-template name="str:_tokenize-characters">
                <xsl:with-param name="string" select="$string"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
            <xsl:call-template name="str:_tokenize-delimiters">
                <xsl:with-param name="string" select="$string"/>
                <xsl:with-param name="delimiters" select="$delimiters"/>
            </xsl:call-template>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>
<xsl:template name="str:_tokenize-characters">
    <xsl:param name="string"/>
    <xsl:if test="$string">
        <token>
            <xsl:value-of select="substring($string, 1, 1)"/>
        </token>
        <xsl:call-template name="str:_tokenize-characters">
            <xsl:with-param name="string" select="substring($string, 2)"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>
<xsl:template name="str:_tokenize-delimiters">
    <xsl:param name="string"/>
    <xsl:param name="delimiters"/>
    <xsl:variable name="delimiter" select="substring($delimiters, 1, 1)"/>
    <xsl:choose>
        <xsl:when test="not($delimiter)">
            <token>
                <xsl:value-of select="$string"/>
            </token>
        </xsl:when>
        <xsl:when test="contains($string, $delimiter)">
            <xsl:if test="not(starts-with($string, $delimiter))">
                <xsl:call-template name="str:_tokenize-delimiters">
                    <xsl:with-param name="string" select="substring-before($string, $delimiter)"/>
                    <xsl:with-param name="delimiters" select="substring($delimiters, 2)"/>
                </xsl:call-template>
            </xsl:if>
            <xsl:call-template name="str:_tokenize-delimiters">
                <xsl:with-param name="string" select="substring-after($string, $delimiter)"/>
                <xsl:with-param name="delimiters" select="$delimiters"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
            <xsl:call-template name="str:_tokenize-delimiters">
                <xsl:with-param name="string" select="$string"/>
                <xsl:with-param name="delimiters" select="substring($delimiters, 2)"/>
            </xsl:call-template>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>
Community
  • 1
  • 1
Andrew DiNunzio
  • 87
  • 2
  • 10
  • **1.** Please post an example of the XML input, as well as the expected result - see: [mcve]. -- **2.** Are you sure your processor supports the `str:tokenize()` function? – michael.hor257k Jan 28 '16 at 18:06
  • Do you expect to be able to put a variable reference into the `match` pattern of the key ``? I don't think XSLT 1.0 allows that. – Martin Honnen Jan 28 '16 at 18:09
  • Sorry about that. I updated the question with input and expected output, along with a sample reference xml file. And @MartinHonnen, yes, I was hoping to have some way to add to the "removals" key based on that variable. Is there another way I could accomplish this same task without using the instruction? Thanks! – Andrew DiNunzio Jan 28 '16 at 18:39
  • @AndrewDiNunzio Frankly, I got lost in your explanation. Why is `Source="$(env.srcDir)\thisWillStay.txt"` included in the output, but `Source="$(env.srcDir)\file1.txt"` is not? – michael.hor257k Jan 28 '16 at 18:54
  • In the referenced xml file (the one with the tag), there is a tag with an attribute Name="my_sdk", which has a Fig with an attribute Directory="OutputFiles". "file1.txt" is in this text node, but "thisWillStay.txt" is not. (my attempt at this xpath is in the xsl:variable "myOutputFiles" declaration I wrote near the top). The files listed in Fig only apply to the `INSTALLDIRECTORY` node, not to any of its subdirectories, which is why `Source="$(env.srcDir)\ThisWillStay\file1.txt` is still included. – Andrew DiNunzio Jan 28 '16 at 19:01
  • @AndrewDiNunzio I understand why `"Source="$(env.srcDir)\ThisWillStay\file1.txt"` is included. I don't understand why `Source="$(env.srcDir)\file1.txt"` is excluded (it contains `file1.txt`), or why `Source="$(env.srcDir)\thisWillStay.txt"` is included (it contains none of the tokens listed in ``. – michael.hor257k Jan 28 '16 at 19:08
  • Oh, sorry, they specify files to remove, not files that should stay. Since the Fig contained file1.txt, `Source="$(env.srcDir)\file1.txt"` is removed from the output. Fig does not contain any path to "thisWillStay.txt", so `Source="$(env.srcDir)\thisWillStay.txt"` is not removed, and the other file1.txt (`Source="$(env.srcDir)\ThisWillStay\file1.txt`) is not removed because it is in a sub directory. – Andrew DiNunzio Jan 28 '16 at 19:15

1 Answers1

3

Let me suggest the following approach:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:param name="parts" select="document('parts.xml')"/>

<xsl:variable name="x-paths">
    <xsl:call-template name="tokenize">
        <xsl:with-param name="text" select="normalize-space($parts//Fig[@Directory='OutputFiles'])"/>
        <xsl:with-param name="delimiter" select="' '"/>
    </xsl:call-template>
</xsl:variable>

<xsl:variable name="x-steps">
    <xsl:for-each select="exsl:node-set($x-paths)/token">
        <xsl:call-template name="tokenize">
            <xsl:with-param name="text" select="."/>
        </xsl:call-template>
    </xsl:for-each>
</xsl:variable>

<xsl:variable name="x-ids">
    <xsl:for-each select="/wix:Wix/wix:Fragment/wix:DirectoryRef/wix:Component">
        <xsl:variable name="steps">
            <xsl:call-template name="tokenize">
                <xsl:with-param name="text" select="wix:File/@Source"/>
                <xsl:with-param name="delimiter" select="'\'"/>
            </xsl:call-template>
        </xsl:variable>
        <xsl:if test="exsl:node-set($steps)/token = exsl:node-set($x-steps)/token">
            <id>
                <xsl:value-of select="@Id"/>
            </id>
        </xsl:if>
    </xsl:for-each>
</xsl:variable>

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

<xsl:template match="wix:Component | wix:ComponentRef">
    <xsl:if test="not(@Id = exsl:node-set($x-ids)/id)">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:if>
</xsl:template>

<xsl:template name="tokenize">
    <xsl:param name="text"/>
    <xsl:param name="delimiter" select="'/'"/>
        <xsl:variable name="token" select="substring-before(concat($text, $delimiter), $delimiter)" />
        <xsl:if test="$token">
            <token>
                <xsl:value-of select="$token"/>
            </token>
        </xsl:if>
        <xsl:if test="contains($text, $delimiter)">
            <!-- recursive call -->
            <xsl:call-template name="tokenize">
                <xsl:with-param name="text" select="substring-after($text, $delimiter)"/>
                <xsl:with-param name="delimiter" select="$delimiter"/>
            </xsl:call-template>
        </xsl:if>
</xsl:template>

</xsl:stylesheet>

Applied to your sample input, the result will be:

<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
   <Fragment>
      <DirectoryRef Id="INSTALLDIRECTORY">
         <Component Id="cmp2A1630B8E0E70C310FC91CD5DADB5A43" Guid="*">
            <File Id="filF5C36F42ADA8B3DD927354B5AB666898" KeyPath="yes" Source="$(env.srcDir)\thisWillStay.txt"/>
         </Component>
         <Directory Id="dir2A411B40F7C80649B57155F53DD7D136" Name="ThisWillStay">
            <Component Id="cmpE7DF6F3C9BE17355EA10D49649C4957A" Guid="*">
               <File Id="fil908CF12B8DD2E95A77D7611874EC3892" KeyPath="yes" Source="$(env.srcDir)\ThisWillStay\file1.txt"/>
            </Component>
         </Directory>
      </DirectoryRef>
   </Fragment>
   <Fragment>
      <ComponentGroup Id="DeliveredFiles">
         <ComponentRef Id="cmp2A1630B8E0E70C310FC91CD5DADB5A43"/>
         <ComponentRef Id="cmpE7DF6F3C9BE17355EA10D49649C4957A"/>
      </ComponentGroup>
   </Fragment>
</Wix>
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • Wow, thanks for the help! This **almost** works. It seems to work perfectly for the first and last item in the `Fig` list, but it doesn't remove any of the files that are in the middle. Also, I initially got an error saying: `Namespace 'http://exslt.org/common' does not contain any functions`, so I replaced it with the namespace `xmlns:msxsl="urn:schemas-microsoft-com:xslt"` and `xmlns:user="http://www.contoso.com"`, replacing all of the `exsl:` prefixes with `msxsl:` prefixes. – Andrew DiNunzio Jan 28 '16 at 23:21
  • @AndrewDiNunzio I am not sure what you mean by "files that are in the middle". I have posted my result. How does it differ from yours? – michael.hor257k Jan 28 '16 at 23:30
  • The `` contains `foo/bar/file1.txt`, `foo/bar/file2.exe`, and `foo/bar/Some.File.dll`. But the code you provided successfully removes nodes related to file1.txt and Some.File.dll (the first and last in the list), but the node related to "file2.exe" stays behind. I'm sorry, I should have provided multiple entries in my sample input. I'll edit my original question and add it to the sample input. Thanks for bearing with me – Andrew DiNunzio Jan 28 '16 at 23:35
  • @AndrewDiNunzio Ok, I see. Try it now. – michael.hor257k Jan 28 '16 at 23:43
  • Ran a few tests, and it seems to work perfectly now! Thank you so much! (marking this as accepted, but I'll be sure to upvote it once I get the necessary reputation points) – Andrew DiNunzio Jan 29 '16 at 00:09