I'm compiling a NAnt project on linux with TeamCity Continuous Integration server. I have been able to generate a test report by running NAnt on mono thru a Command Line Runner but don't have the options of using the report like a NAnt Runner. I'm also using MBUnit for the testing framework.
How can I merge in the test report and display "Tests failed: 1 (1 new), passed: 3049" for the build?
Update: take a look at MBUnitTask its a NAnt task that uses sends messages that TeamCity expects from NUnit so it lets you use all of TeamCity's features for tests.
MBUnitTask
Update: Galio has better support so you just have to reference the Galio MBUnit 3.5 dlls instead of the MBUnit 3.5 dlls and switch to the galio runner to make it work.
Gallio now has an extension to output TeamCity service messages.
Just use the included Gallio.NAntTasks.dll and enable the TeamCity extension. (this won't be necessary in the next release)
TeamCity watches the command line output from the build. You can let it know how your tests are going by inserting certain markers into that output See http://www.jetbrains.net/confluence/display/TCD3/Build+Script+Interaction+with+TeamCity. For example
##teamcity[testSuiteStarted name='Test1']
will let TeamCity know that a set of tests started. With MbUnit you can't output these markers while the tests are running, but you can transform the XML file that it outputs. Here is the XSL that I am using:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="assemblies/assembly">
##teamcity[testSuiteStarted name='<xsl:value-of select="#name" />']
<xsl:apply-templates select="//run" />
##teamcity[testSuiteFinished name='<xsl:value-of select="#name" />']
</xsl:template>
<xsl:template match="run">
<xsl:choose>
<xsl:when test="#result='ignore' or #result='skip'">
##teamcity[testIgnored name='<xsl:value-of select="#name" />' message='Test Ignored']
</xsl:when>
<xsl:otherwise>
##teamcity[testStarted name='<xsl:value-of select="#name" />']
</xsl:otherwise>
</xsl:choose>
<xsl:if test="#result='failure'">
##teamcity[testFailed name='<xsl:value-of select="#name" />' message='<xsl:value-of select="child::node()/message"/>' details='<xsl:value-of select="normalize-space(child::node()/stack-trace)"/>']
</xsl:if>
<xsl:if test="#result!='ignore' and #result!='skip'">
##teamcity[testFinished name='<xsl:value-of select="#name" />']
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Here's what I came up with
How can I merge in the test report?
First you'll need to get mbunit to generate both an XML and HTML report. The Command line arguments look like this
/rt:Xml /rt:Html /rnf:mbunit /rf:..\reports
this will generate the reports into a dir called reports and the file will be called mbunit.xml and mbunit.html
next we want to add these files as artifacts on the build
build\reports\* => Reports
the last step is to tell teamcity to add it as a tab for the build
find the .BuildServer\config\main-config.xml and add this line
(on windows this is in c:\Documents and Settings\, on linux it was in the /root dir)
<report-tab title="Tests" basePath="Reports" startPage="mbunit.html" />
How can I display "Tests failed: 1 (1 new), passed: 3049" for the build?
TeamCity looks for a file called teamcity-info.xml where you can stick messages in to be displayed. The Actual test count is actually just plain text. I think you can just add the file as an artifact but I've also got it in the root dir of the build.
in NAnt you'll want to use this command to do an XSLT on the MBUnit XML Report
<style style="includes\teamcity-info.xsl" in="reports\mbunit.xml" out="..\teamcity-info.xml" />
the actual xsl looks like this.
(Note: that the { and } are reserved in xsl so we have to use params)
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:param name="cbl" select="'{'"/>
<xsl:param name="cbr" select="'}'"/>
<xsl:template match="/">
<xsl:for-each select="report-result/counter">
<build number="1.0.{concat($cbl,'build.number',$cbr)}">
<xsl:if test="#failure-count > 0">
<statusInfo status="FAILURE">
<text action="append"> Tests failed: <xsl:value-of select="#failure-count"/>, passed: <xsl:value-of select="#success-count"/></text>
</statusInfo>
</xsl:if>
<xsl:if test="#failure-count = 0">
<statusInfo status="SUCCESS">
<text action="append"> Tests passed: <xsl:value-of select="#success-count"/></text>
</statusInfo>
</xsl:if>
</build>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This will give you a file that looks like this
<build number="1.0.{build.number}">
<statusInfo status="FAILURE">
<text action="append">Tests failed: 16, passed: 88</text>
</statusInfo>
</build>
TeamCity Sidebar Gadget for Windows Vista, Windows 7
http://teamcity-gadget.com
Related
XALANC-421 (omit-xml-declaration ignored) is supposed to be fixed in Xalan 1.11.0 according to the release notes at https://apache.github.io/xalan-c/releases.html. However, I still observe this issue until Xalan 1.12.0.
Sample files from Ch. 6 of Learning XSLT were used in my testing.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="eu">
<xsl:apply-templates select="member"/>
</xsl:template>
<xsl:template match="member">
<eu-members>
<xsl:apply-templates select="state[#founding]"/>
</eu-members>
</xsl:template>
<xsl:template match="state">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Shortened version of input file
<?xml version="1.0" encoding="UTF-8"?>
<!-- European Union member states and candidate states -->
<eu>
<member>
<state>Austria</state>
<state founding="yes">Belgium</state>
<state>Denmark</state>
</member>
<candidate>
<state>Bulgaria</state>
<state>Cyprus</state>
</candidate>
</eu>
Output still has the xml declaration using xalan 1.11.0
<?xml version="1.0" encoding="UTF-8"?>
<eu-members xmlns:xml="http://www.w3.org/XML/1998/namespace">
<state>Belgium</state>
</eu-members>
Output using xalan 1.12.0
<eu-members>
<state>Belgium</state>
</eu-members>
I am trying to process my .coveragexml file (after converting the .coverage file) that I get after using MSTest from the command line but Sonar Runner keeps failing as it tries to parse the file.
The errors consist of parsing errors such as an unexpected '?' as well as not being able to find the tag in the file.
I have tried a few ways to get the .coveragexml file: using the "vsinstr -coverage ..." and "start vsperfmon -coverage ..." commands (then running MSTest) from the command line, changing
the .testrunconfig file and indicating which dlls I want to get coverage for, and tried using "CodeCoverage.exe collect ...". The first two have given me success on getting code coverage data,
but I have had issues getting "CodeCoverage.exe collect ..." to collect results. Even though I can get collect code coverage results from the first two, the .coveragexml file that is
produced does not seem to be in the right format that SonarQube accepts, even though they indicate on their VB.NET plugin webpage that they support MSTest and VSTest XML code coverage files.
I have tried using VSTest and can get my .coveragexml files to be accepted by Sonarqube without any errors just fine. The problem is that the company I am interning for uses MSTest to run all
of their unit tests, so I need to get .coveragexml data from using MSTest.
Another thing I noticed was that when I try to export the .coverage file as a .coveragexml within Visual Studio (for both MSTest or VSTest), it produces a .coveragexml format that Sonarqube
doesn't accept (it just errors out due to the errors I mentioned above). When I use the "CodeCoverage.exe analyze ..." command to convert the .coverage file from VSTest, it produces a
.coveragexml format that Sonarqube accepts as I receive no errors and can see my code coverage results on the dashboard. Now when I try to use "CodeCoverage.exe analyze ..." command to convert
the .coverage file from MSTest, nothing happens. No .coveragexml file is produced and no errors or any sort of feedback is given. I have also tried writing a C# method to convert the .coverage
file to a .coveragexml file using Microsoft.VisualStudio.Coverage.Analysis. But it produces the same format .coveragexml file as if I were exporting it from Visual Studio.
Other things that might be helpful to know:
I am running the analysis on VB.NET code.
I am using version 2.2 of the VB.NET plugin from Sonarqube.
I am using version 4.3.2 of Sonarqube and version 2.4 of the SonarQube Runner.
I am using Visual Studio 2013 Premium.
(SonarQube errors out)
The format of the .coveragexml file after exporting it from Visual Studio is like this:
<CoverageDSPriv>
<xs:schema ...>
...
</xs:schema>
<Module>
<ModuleName>...</ModuleName>
<ImageSize>...</ImageSize>
...
<NameSpaceTable>
<BlocksCovered>...</BlocksCovered>
...
(SonarQube accepts)
The format of the .coveragexml file after using "CodeCoverage.exe analyze ..." (only works with VSTest's .coverage file)
<?xml version="1.0" encoding="UTF-8" ?>
<results>
<modules>
<module name="..." path="..." id="..." block_coverage="..." line_coverage="..." blocks_covered="..." ... >
<functions>
<function id="..." token="..." name="..." type_name="..." block_coverage="..." >
...
It looks like there are two completely different schemas for this data and SonarQube is only accepting one of them, is that the case? Is there another way to convert the .coverage data to the one that SonarQube accepts?
I have created this XSLT to convert the coveragexml file in the good format.
<xsl:output method="xml" indent="yes"/>
<xsl:template match="CoverageDSPriv">
<results>
<modules>
<xsl:for-each select="Module">
<xsl:element name="module">
<xsl:attribute name="name">
<xsl:value-of select="ModuleName"/>
</xsl:attribute>
<xsl:attribute name="path">
<xsl:value-of select="ModuleName"/>
</xsl:attribute>
<xsl:attribute name="block_coverage">
<xsl:value-of select="BlocksCovered div (BlocksCovered + BlocksNotCovered) * 100"/>
</xsl:attribute>
<xsl:attribute name="line_coverage">
<xsl:value-of select="LinesCovered div (LinesCovered + LinesPartiallyCovered + LinesNotCovered) * 100"/>
</xsl:attribute>
<xsl:attribute name="blocks_covered">
<xsl:value-of select="BlocksCovered"/>
</xsl:attribute>
<xsl:attribute name="blocks_not_covered">
<xsl:value-of select="BlocksNotCovered"/>
</xsl:attribute>
<xsl:attribute name="lines_covered">
<xsl:value-of select="LinesCovered"/>
</xsl:attribute>
<xsl:attribute name="lines_partially_covered">
<xsl:value-of select="LinesPartiallyCovered"/>
</xsl:attribute>
<xsl:attribute name="lines_not_covered">
<xsl:value-of select="LinesNotCovered"/>
</xsl:attribute>
<xsl:for-each select="NamespaceTable">
<xsl:for-each select="Class">
<functions>
<xsl:for-each select="Method">
<xsl:element name="function">
<xsl:attribute name="name">
<xsl:value-of select="substring-before(MethodName, '()')"/>
</xsl:attribute>
<xsl:attribute name="type_name">
<xsl:value-of select="../ClassName"/>
</xsl:attribute>
<xsl:attribute name="block_coverage">
<xsl:value-of select="BlocksCovered div (BlocksCovered + BlocksNotCovered) * 100"/>
</xsl:attribute>
<xsl:attribute name="line_coverage">
<xsl:value-of select="LinesCovered div (LinesCovered + LinesPartiallyCovered + LinesNotCovered) * 100"/>
</xsl:attribute>
<xsl:attribute name="blocks_covered">
<xsl:value-of select="BlocksCovered"/>
</xsl:attribute>
<xsl:attribute name="blocks_not_covered">
<xsl:value-of select="BlocksNotCovered"/>
</xsl:attribute>
<xsl:attribute name="lines_covered">
<xsl:value-of select="LinesCovered"/>
</xsl:attribute>
<xsl:attribute name="lines_partially_covered">
<xsl:value-of select="LinesPartiallyCovered"/>
</xsl:attribute>
<xsl:attribute name="lines_not_covered">
<xsl:value-of select="LinesNotCovered"/>
</xsl:attribute>
<ranges>
<xsl:for-each select="Lines">
<xsl:element name="range">
<xsl:attribute name="source_id">
<xsl:value-of select="SourceFileID"/>
</xsl:attribute>
<xsl:attribute name="covered">
<xsl:choose>
<xsl:when test="Coverage=0">yes</xsl:when>
<xsl:when test="Coverage=1">partial</xsl:when>
<xsl:when test="Coverage=2">no</xsl:when>
</xsl:choose>
</xsl:attribute>
<xsl:attribute name="start_line">
<xsl:value-of select="LnStart"/>
</xsl:attribute>
<xsl:attribute name="start_column">
<xsl:value-of select="ColStart"/>
</xsl:attribute>
<xsl:attribute name="end_line">
<xsl:value-of select="LnEnd"/>
</xsl:attribute>
<xsl:attribute name="end_column">
<xsl:value-of select="ColEnd"/>
</xsl:attribute>
</xsl:element>
</xsl:for-each>
</ranges>
</xsl:element>
</xsl:for-each>
</functions>
<source_files>
<xsl:for-each select="../../../SourceFileNames">
<xsl:element name="source_file">
<xsl:attribute name="id">
<xsl:value-of select="SourceFileID"/>
</xsl:attribute>
<xsl:attribute name="path">
<xsl:value-of select="SourceFileName"/>
</xsl:attribute>
</xsl:element>
</xsl:for-each>
</source_files>
</xsl:for-each>
</xsl:for-each>
</xsl:element>
</xsl:for-each>
</modules>
</results>
</xsl:template>
I think you've perfectly described the situation: There indeed are two totally different code coverage reports, which both uses the *.coveragexml extension.
The C# and VB.NET plugin only support one of those formats at the moment, and a ticket exists to add the support for the other: https://jira.codehaus.org/browse/SONARNTEST-3
I'm trying to build an installer that includes a number of features and I'm using heat to harvest a directory of files for each feature.
My source directory structure looks something like this:
HarvestDir
\FeatureA
\FeatureImpl.dll
\FeatureImpl2.dll
\FeatureB
\FeatureImpl.dll
\FeatureImpl2.dll
So I execute heat.exe for each feature to create a fragment for each feature but I get basically identical fragments e.g.
[...] Source="SourceDir\FeatureImpl.dll"
[...] Source="SourceDir\FeatureImpl2.dll"
What I really want is something like this:
[...] Source="SourceDir\FeatureA\FeatureImpl.dll"
[...] Source="SourceDir\FeatureA\FeatureImpl2.dll"
and
[...] Source="SourceDir\FeatureB\FeatureImpl.dll"
[...] Source="SourceDir\FeatureB\FeatureImpl2.dll"
I could use -var to specify a separate variable to represent the source location for each feature, but then I'd have to pass values for these variables into the wixproj (and I'm going to have ~10 features).
So, is there any way I can include a relative path in my harvested fragment?
Rather than using heat on each Feature* folder, you could just harvest the whole feature layout folder that you have created and have heat transform the results. I'm assuming that each subfolder is a feature. It's a simple matter of creating a ComponentGroup for each one that references all the components under it.
You'd then use the component groups like this:
<Feature Id="FeatureA">
<ComponentGroupRef Id="FeatureA"/>
</Feature>
<Feature Id="FeatureB">
<ComponentGroupRef Id="FeatureB"/>
</Feature>
Heat command (I actually use the HarvestDirectory target in WiX 3.7):
Heat.exe dir FeatureLayout -dr FeatureLayoutDir -srd -ag -sfrag -t FeatureLayout.xslt -out obj\Debug\__dir.wxs
FeatureLayout.xslt:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0"
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="wix"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="wix:Wix">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
<xsl:for-each select="wix:Fragment/wix:DirectoryRef/wix:Directory">
<wix:Fragment>
<wix:ComponentGroup Id="{#Name}">
<xsl:for-each select="descendant::wix:Component">
<wix:ComponentRef Id="{#Id}"/>
</xsl:for-each>
</wix:ComponentGroup>
</wix:Fragment>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
You can either assign your source paths in .wixproj in DefineConstants or you can assign them in a WIX include file but either way you will have to use "var" option to specify the variable being used for your sources.
If you want to use DefineConstant in wixproj then you will have to do something like
<Target Name="BeforeBuild">
<PropertyGroup>
<DefineConstants>BINFOLDER=..\PATH\TO\SOURCE\$(Configuration)</DefineConstants>
</PropertyGroup>
<HeatDirectory OutputFile="output.wxs" Directory="..\PATH\TO\SOURCE\$(Configuration)" DirectoryRefId="INSTALLFOLDER" ComponentGroupName="COMPONENTGROUPNAME" ToolPath="$(WixToolPath)" PreprocessorVariable="var.BINFOLDER" />
</Target>
In Heat task make sure you add any additional attributes that you might need. In case you have added a reference to your source project in the .wixproj then you may directly use the project reference variables in the "PreprocessorVariable" attribute. For example instead of var.BINFOLDER use var.MyProject.TargetDir.
If you want to use a WIX include file (.wxi) then declare your variables in an include file for example Variables.wxi:
<?xml version="1.0" encoding="UTF-8"?>
<Include>
<?define PATH1="PATH\TO\SOURCE"?>
<?define PATH2="$(var.PATH1)\$(Configuration)"?>
</Include>
Now you will need to pass in PATH1 and PATH2 using the -var in the heat command line. Once you have your .wxs file you will then need to include the Variables.wxi file in that using
<?include Variables.wxi ?>
in your .wxs. The problem with this way is that you will have to either add this include directive manually each time you generate your WIX source files or you will have to inject it using XSL Transformation.
hit properties for the setup project, this is the command i'm using:
"%WIX%\bin\heat.exe" dir "$(SolutionDir)\Export\Release" -suid -dr bin -srd -cg ExportComponentGroup -var var.sourcePath -ag -sreg -out "$(SolutionDir)\SetupProject\AppExportDir.wxs"
the only thing else you need to define in the build tab is:
check the check box for "define 'debug'preprocessor variable" and enter.
sourcePath=%SystemDrive%\App\Main\Export\Release\
this please make sure that you use the flags that you want like suid or -dr
for more information about the flags you have see here:
http://wix.sourceforge.net/manual-wix3/heat.htm
I have a WiX installer to install and start a service. However, all the examples I find place the ServiceInstall tag directly below the file tag for the .exe file to be installed as a service.
I can’t do this as I am using heat to generate my file elements in a separate file. So my wix script looks like this:
<Directory Id="INSTALLLOCATION" Name="Email Generation Service">
<Component Id="SetupService" Guid="51E78696-80E0-4CDA-8F49-902C67CB129C">
<CreateFolder />
<ServiceInstall Id="ServiceInstaller"
Type="ownProcess"
Vital="yes"
Name="EmailGenerationService"
DisplayName="Email Generation Service"
Description="Service for generating Emails from Nexus"
Start="auto"
Account="LocalService"
ErrorControl="ignore"
Interactive="no">
</ServiceInstall>
<ServiceControl Id="StartService" Start="install" Stop="both" Remove="uninstall" Name="EmailGenerationService" Wait="yes" />
</Component>
</Directory>
How can I tell WiX which file I want to install as a service?
I have used XSLT to set the KeyPath on all files to no, with the exception of the file I want to install, despite the fact that all files are in their own component. I am at a bit of a loss now :(
A service must be linked to a specific file. This is a Windows Installer limitation. So one way or another you need to create the ServiceInstall element under your EXE file element.
A solution would be to hard-code the EXE file instead of letting it be generated automatically.
Adding this as relevant information, I had a similar problem and used a similar solution to you, adding an xml transform. However, I used the transform to insert the service control/install elements into the heat-generated fragment. I've pasted my transform below, you may have to modify properties or remove items you don't need.
Some things to note:
The service control doesn't auto-start the service on install, I had
issues with assembly refs not being populated by the time the msi tried to invoke them
A key part of the XSLT adds
an "include" statement at the top of the heat.exe file so that I can
reference variables from my .wxi file
This assumes the variable name
you told heat to inject as your -var parameter is "var.SourcePath"
I never extracted the name of the .exe file or settings file out as
variables that can be injected into the transform because they were
fairly stable and it seemed...difficult
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
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">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<!-- Template for the new ServiceInstall element -->
<xsl:param name="pServiceInstall">
<xsl:element name="ServiceInstall">
<xsl:attribute name="Id">SVINSTL_$(var.ServiceName)</xsl:attribute>
<xsl:attribute name="Description">$(var.ServiceInstallDescription)</xsl:attribute>
<xsl:attribute name="Account">$(var.SystemAccount)</xsl:attribute>
<xsl:attribute name="DisplayName">$(var.ServiceInstallDisplayName)</xsl:attribute>
<xsl:attribute name="ErrorControl">normal</xsl:attribute>
<xsl:attribute name="Name">$(var.ServiceName)</xsl:attribute>
<xsl:attribute name="Interactive">no</xsl:attribute>
<xsl:attribute name="Start">auto</xsl:attribute>
<xsl:attribute name="Type">ownProcess</xsl:attribute>
<xsl:attribute name="Vital">yes</xsl:attribute>
</xsl:element>
</xsl:param>
<!-- Template for the new ServiceControl element -->
<xsl:param name="pServiceControl">
<xsl:element name="ServiceControl">
<xsl:attribute name="Id">SVCTRL_$(var.ServiceName)</xsl:attribute>
<xsl:attribute name="Name">$(var.ServiceName)</xsl:attribute>
<xsl:attribute name="Stop">both</xsl:attribute>
<xsl:attribute name="Remove">uninstall</xsl:attribute>
<xsl:attribute name="Wait">yes</xsl:attribute>
</xsl:element>
</xsl:param>
<!-- Insert a ?include statement at the top of the fragment so it can use config variables -->
<xsl:template match="wix:Wix">
<xsl:copy>
<xsl:processing-instruction name="include">InstallerSettings.wxi</xsl:processing-instruction>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<!-- Turn the file name of the executable into a variable so it can be targeted for shortcuts,etc -->
<xsl:template match="#Source[. ='$(var.SourcePath)\HARD_CODED_NAME_OF_PROJECT.exe']">
<xsl:call-template name="identity" />
<xsl:attribute name="Id">$(var.ServiceExecutableFileId)</xsl:attribute>
</xsl:template>
<!-- Insert the ServiceInstall and ServiceControl elements into the component with the exe file -->
<xsl:template match="//wix:File[#Source='$(var.SourcePath)\HARD_CODED_NAME_OF_PROJECT.exe']">
<xsl:call-template name="identity" />
<xsl:copy-of select="$pServiceInstall"/>
<xsl:copy-of select="$pServiceControl"/>
</xsl:template>
<!-- Identity template (copies everything as is) -->
<xsl:template match="#*|node()" name="identity">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
I have a lot files to harvest in a per user install project in wix.
I used heat.exe to harvest the file, but each file in one component has its own keypath property, while my files will copy to "app data" so it has to use a registry key under HKCU as its KeyPath, so I have to change each item in the XML file.
Can it be done by heat.exe? I have thousands of files to harvest, it is terrible to fix it manually.
Use this xslt to customize KeyPath item for nodes that have child nodes.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl"
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi"
xmlns:my="my:my">
<xsl:output method="xml" indent="yes" />
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match='wix:Wix/wix:Fragment/wix:ComponentGroup/wix:Component'>
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:attribute name="KeyPath">
<xsl:text>no</xsl:text>
</xsl:attribute>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
derived from #KirillPolishchuk 's answer https://stackoverflow.com/a/8035049/483588
As far as I know, heat doesn't support this out-of-the-box. However, you can apply an XSL template to the heat output and tweak the final wxs file the way you'd like. See -t: switch of heat.exe for more details.