The basic idea is to filter out the component of "MyExe.exe" from the Heat output, then manually add it back to the main WiX authoring, where we can specify a constant File/@Id
attribute.
The filtering can be done by passing a XSLT file to Heat:
<HeatDirectory Transforms="$(ProjectDir)RemoveMyExeComponent.xslt" ... />
RemoveMyExeComponent.xslt could look as follows (idea from this answer, but simplified for current question). In this file, replace MyExe.exe
with the actual name of your EXE and replace - 8
by the length of the file name, decremented by one.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi"
xmlns="http://schemas.microsoft.com/wix/2006/wi"
version="1.0"
exclude-result-prefixes="xsl wix">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*" />
<!--
Find the component of the main EXE and tag it with the "ExeToRemove" key.
Because WiX's Heat.exe only supports XSLT 1.0 and not XSLT 2.0 we cannot use `ends-with( haystack, needle )` (e.g. `ends-with( wix:File/@Source, '.exe' )`...
...but we can use this longer `substring` expression instead (see https://github.com/wixtoolset/issues/issues/5609 )
-->
<xsl:key
name="ExeToRemove"
match="wix:Component[ substring( wix:File/@Source, string-length( wix:File/@Source ) - 8 ) = 'MyExe.exe' ]"
use="@Id"
/> <!-- In this expression "-8" is the length of "MyExe.exe" - 1 because XSLT uses 1-based indexes, not 0-based indexes. -->
<!-- By default, copy all elements and nodes into the output... -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
<!-- ...but if the element has the "ExeToRemove" key then don't render anything (i.e. removing it from the output) -->
<xsl:template match="*[ self::wix:Component or self::wix:ComponentRef ][ key( 'ExeToRemove', @Id ) ]" />
</xsl:stylesheet>
Now that we have removed the generated EXE component, we can manually add it back to the WiX authoring, giving us full control over all attributes:
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="WixHeatConstantExeFileId" Language="1033" Version="!(bind.fileVersion.myExe)" Manufacturer="zett42" UpgradeCode="PUT-GUID-HERE">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate />
<Feature Id="ProductFeature" Title="WixHeatConstantExeFileId" Level="1">
<ComponentGroupRef Id="ProductComponents" />
<ComponentGroupRef Id="HarvestedComponents" />
</Feature>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="WixHeatConstantExeFileId" />
</Directory>
</Directory>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<!-- Manually add back the EXE component that was filtered out from Heat output. -->
<Component Id="myExe" Guid="*">
<File Id="myExe" KeyPath="yes" Source="SourceDir\MyExe.exe" />
</Component>
</ComponentGroup>
</Product>
</Wix>
Works like a charm in my test project!