WiX Heat: sort order of components - sorting

I want to be able to compare the WXS file - generated by heat - with the previous version so that I can verify the changes.
But I can't find any logic behind the component order. The generated components are neither sorted by Id nor by directory name. And always when I regenerate my WXS files and some files have been added, the order is totally different, which makes the comparation very difficult.
Is there any way to control the sort order? Ideal would be, if the components would be sorted by the source of inner file node. But any constant order would be good enough for me.
E.g. for the following example, it would be great, when the components are sorted by file source. I.E. AutoMapper before Log4Net
<Component Id="cmpB83..." Directory="DIR.MYDIR" Guid="{2A49...}">
<File Id="filA272..." KeyPath="yes" source="$(var.MYVAR)\AutoMapper.xml" />
</Component>
<Component Id="cmp445..." Directory="DIR.MYDIR" Guid="{1C34...}">
<File Id="filB356..." KeyPath="yes" source="$(var.MYVAR)\Log4Net.xml" />
</Component>
I start heat with following parameters:
heat.exe dir MYDIR -gg -dr DIR.MYDIR -srd -sreg -ke -cg compGroup.MYGROUP -var var.MYVAR -out ..\MYOUTPUT.wxs
I use WiX 3.11.

Since there is no answer since two years, I will show what I have done:
I use this code to re-order the components via "source" in the generated file and store it to a copy:
XDocument xdoc = XDocument.Load(filename);
XElement componentsFragment = xdoc.Root.Elements().ElementAt(1);
XElement componentGroup = componentsFragment.Elements().ElementAt(0);
List<XElement> components = componentGroup.Elements().ToList();
List<XElement> sortedComponents = components.OrderBy(one => one.Elements().ElementAt(0).Attribute("Source").Value).ToList();
componentGroup.RemoveNodes();
foreach (XElement component in sortedComponents)
{
componentGroup.Add(component);
}
xdoc.Save(resultFilename);

Related

How to use an msbuild CopyTask to copy a list of directories recursively

I would like to copy a list of directories recursively using a CopyTask.
The list is defined by a macro like so;
<ConanBinaryDirectories>some/path/;another/path/;</ConanBinaryDirectories>
I know a CopyTask can copy a single directory recursively, but how to deal with the specified format.
The ConanBinaryDirectories seems to be a MSBuild Property. If so, I assume you can use Msbuild Property Functions to get the single path.
Something like this:
<PropertyGroup>
<ConanBinaryDirectories>C:\Users\xxx\Desktop\Path1;C:\Users\xxx\Desktop\Path2;</ConanBinaryDirectories>
</PropertyGroup>
<PropertyGroup>
<SourcePath1>$(ConanBinaryDirectories.Split(";")[0])</SourcePath1> //C:\Users\xxx\Desktop\Path1
<SourcePath2>$(ConanBinaryDirectories.Split(";")[1])</SourcePath2> //C:\Users\xxx\Desktop\Path2
</PropertyGroup>
After you get the property which represents the single directory, you can use either 1.Copy task or 2.Exec task with xcopy command in it like this to copy the single directory to destination path.
All you need to do is to call the corresponding task twice in your custom target.
I know maybe what you want when you ask this question is a way like turn the MSBuild property to an MSBuild Item as the input of a task and do the copy job. But after my check: 1. The msbuild copy task actually doesn't support the input format like this some/path/ 2.We can use something like some/path/**/*.*, but it doesn't work well when our input could be something like #(...)/**/*.*.
So I suggest you split the macro to several paths and then use them into copy job.
Update:
The msbuild property doesn't support wildcard well. So to use something like **/*.*, you need to use Item instead of Property. You can have a look at this similar issue.
For a Property whose value is Path/*.*, it only represents a string Path/*.* most of the time while for an Item <MyItem Include="Path/*.*"/>, it represents all the files in the specified path. So no matter which way(copy task or xcopy command) we choose to do the copy job,the input should be an Item.
The script which works after test:
<PropertyGroup>
C:\Users\xxx\Desktop\Path1;C:\Users\xxx\Desktop\Path2
<PropertyGroup>
<SourcePath1>$(ConanBinaryDirectories.Split(";")[0])</SourcePath1>
<SourcePath2>$(ConanBinaryDirectories.Split(";")[1])</SourcePath2>
</PropertyGroup>
<ItemGroup>
<MySourceFiles Include="$(SourcePath1)\**\*.*" />
<MySourceFiles Include="$(SourcePath2)\**\*.*" />
</ItemGroup>
<Target Name="TestItem" AfterTargets="build">
<Copy SourceFiles="#(MySourceFiles)" DestinationFolder="$(OutputPath)"/>
</Target>
$(OutputPath) for C#, $(OutDir) for C++.

Why is there a need for a separate item in my MSBuild file?

There are many articles (like this and this) that show how to add files to be published, and they all say to add something like this to the publish profile (.pubxml):
<Target Name="CustomCollectFiles">
<ItemGroup>
<_CustomFiles Include="..\Extra Files\**\*" />
<FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
<DestinationRelativePath>Extra Files\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
</ItemGroup>
</Target>
Why is there a need for the new _CustomFiles item? Why not simply <FilesForPackagingFromProject Include="..\Extra Files\**\*">? I tried it, and for some reason this causes every file in the project to end up in the deployed Extra Files folder. Can someone explain me this behaviour please?
Since you are asking about why this is required, I will have to dive deep into what this code means to explain what you're seeing. <Message /> is our friend!
The meaning of %
Let's first look at what % means by using it in a <Message> task:
<ItemGroup>
<_CustomFiles Include="..\Extra Files\**\*" />
</ItemGroup>
<Message Text="File: %(_CustomFiles.Identity)" />
When you run this, you'll get the following output:
File: ..\Extra Files\file1.txt
File: ..\Extra Files\file2.txt
File: ..\Extra Files\file3.txt
...
File: ..\Extra Files\etc.txt
Basically, the Message task runs once for each item in the item group, because we used %.
What's in the item group?
Let's take a peek at the item group before we even make any changes to it. When this task begins, FilesForPackagingFromProject already has all of the files in them, with various metadata properties, including DestinationRelativePath. Let's see it by adding just this to our task:
<Message Text="File: %(FilesForPackagingFromProject.Identity) -> %(FilesForPackagingFromProject.DestinationRelativePath)" />
This outputs:
File: ..\obj\TempBuildDir\PrecompiledApp.config -> PrecompiledApp.config
File: ..\obj\TempBuildDir\Web.config -> Web.config
File: ..\obj\TempBuildDir\App_Themes\theme.css -> App_Themes\theme.css
...
It's important to realise that this item group is not empty to begin with. You are trying to add items to it.
The working code
When you have sub-elements in an element that has %, they apply once to each iteration, so let's now look at the working code:
<FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
<DestinationRelativePath>Extra Files\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
<Message Text="File: %(FilesForPackagingFromProject.Identity) -> %(FilesForPackagingFromProject.DestinationRelativePath)" />
For each item in _CustomFiles, we include it into the FilesForPackagingFromProject item group and set the DestinationRelativePath metadata property to the appropriate RecursiveDir/Filename values - basically the ones that apply for the current element being looked at. Let's look at what this outputs:
File: ..\obj\TempBuildDir\PrecompiledApp.config -> PrecompiledApp.config
File: ..\obj\TempBuildDir\Web.config -> Web.config
File: ..\obj\TempBuildDir\App_Themes\theme.css -> App_Themes\theme.css
...
File: ..\Extra Files\file1.txt -> Extra Files\file1.txt
File: ..\Extra Files\file2.txt -> Extra Files\file2.txt
File: ..\Extra Files\file3.txt -> Extra Files\file3.txt
...
File: ..\Extra Files\etc.txt -> Extra Files\etc.txt
Including just a single file
If you wanted to include just a single file, you can do so as follows:
<FilesForPackagingFromProject Include="..\Extra Files\file1.txt">
<DestinationRelativePath>Extra Files\file1.txt</DestinationRelativePath>
</FilesForPackagingFromProject>
This has no % to expand anywhere, so it does exactly what you would expect: it includes a single file into the output.
The broken code
Now let's try to include a single file, but without hard-coding the path and instead using the % expression from the original code:
<FilesForPackagingFromProject Include="..\Extra Files\file1.txt">
<DestinationRelativePath>Extra Files\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
<Message Text="File: %(FilesForPackagingFromProject.Identity) -> %(FilesForPackagingFromProject.DestinationRelativePath)" />
There are % here so things get expanded, but because this doesn't have a % in the item group element, the expansion works differently and things get pear-shaped:
File: ..\obj\TempBuildDir\PrecompiledApp.config -> PrecompiledApp.config
File: ..\obj\TempBuildDir\Web.config -> Web.config
File: ..\obj\TempBuildDir\App_Themes\theme.css -> App_Themes\theme.css
...
File: ..\Extra Files\file1.txt -> Extra Files\PrecompiledApp.config
File: ..\Extra Files\file1.txt -> Extra Files\Web.config
File: ..\Extra Files\file1.txt -> Extra Files\theme.css
So instead of adding file1.txt to the item group once, it iterates over the entire collection and adds file1.txt once for each file already in it. RecursiveDir is not set in this context, while Filename/Extension are the original filename of each file in the group.
Hopefully you can see now that this will create a file for each file in your entire deployment, but in a flat tree, and notably, the contents will be that of file1.txt rather than the original file.
When you include a wildcard instead of just one file, the same thing happens for every file matched by the wildcard.
How to fix this
Stick with the %(_CustomFiles) fix. Hopefully you will now see why it's necessary and how it does what it does. I do believe this is how you are supposed to do this: here's another question about it, with an answer that recommends this approach.

Copying directory structure using wix

So I have a huge file structure which I want the installer, I am building using wix, to copy on the client location. Currently I am typing it out like:
<Directory Id="xyz" Name = "abc FileSource = "sdfsdf">
<Component Id="asdas" Guid="asdasd">
<File Id = "asdfgrs" Name="name" />
</Component>
</Directory>
As the number of files have increased I would like this to be done in an automated way. Using heat I am able to generate:
<ComponentGroup Id="weqw">
<Component Id="2132312" Directory="Some random string (cause of concern>" Guid="asdasd">
<File Id="sdqwdqwd> keyPath="yes" Source = "Correct source path" />
</Component>
<ComponentGroup>
My concern is that due to the presence of some random string in Directory field of Component generated by heat, I wont get the directory structure replicated. Is this true? Is there a way around this?
From the heat /? output:
-dr directory reference to root directories (cannot contains spaces
e.g. -dr MyAppDirRef)
If you use heat to recursively harvest a directory structure, then the -dr switch will set the ID of the root target folder. This ID should match the ID of a Directory element you have specified elsewhere in your wxs files.
For the harvested subfolders, heat will still generate a random ID. A given ID will appear multiple times in the generated XML file:
In the Directory element generated by heat for that subfolder, as the Id attribute.
In the Component elements associated with that folder, As the Directory attribute.
The ID is only used to link Component elements to Directory elements. It is not the folder name as it appears after installation. It is only used as a key in the Windows Installer database.

How to get Chirpy to update mashed files when edits are made

I've just begun using Chirpy, and it's frigging amazing. One problem I'm having is that I'm unable to get it to update the mashed file(s) when an edit is made in one of the "sub" files.
IE: If I have a mashed file called "site.css" (via my js.chirp.config file) which contains "elements.css", "master.css", "misc.css" etc. And I make an edit to one of them (say, master.css), I want chirpy to kick in and redo the site.css with the new edits.
Is this at all possible?
Chirpy does this - just make sure your paths use backslashes rather than forward slashes.
<root>
<FileGroup Name="site.css">
<File Path="css\elements.css" />
<File Path="css\master.css" />
<File Path="css\misc.css" />
</FileGroup>
</root>
Chirpity chirp chirp chirp.
I have
<Folder Pattern="*.min.css" Minify="false" />
And with that wildcard in there, it doesnt run when min.css files are updated. I have to update the config and save for the mash to occur

Extracting files from merge module

All I want is a command-line tool that can extract files from a merge module (.msm) onto disk. Said differently, I want the same "administrative install" functionality that is possible for an MSI:
msiexec /a myProduct.msi TARGETDIR="C:\myInstallation" /qn
The above only works on an msi (near as I can tell). So to get the same effect for a merge module, I'm trying msidb.exe and orca.exe The documentation for orca states:
Many merge module options can be
specified from the command line...
Extracting Files from a Merge Module
Orca supports three different methods
for extracting files contained in a
merge module. Orca can extract the
individual CAB file, extract the files
into a module tree and extract the
files into a source image once it has
been merged into a target database...
Extracting Files
To extract the individual files from a
merge module, use the
... -x ... option on the
command line, where is the
desired path to the new directory
tree.
The specified path is used as the root
path for the extracted files. All
files are extracted from the CAB file
embedded in the module and placed in
the specified path. The directory
layout for the extracted files is
based on the directory tree of the
merge module.
It sounds like what I need. But when I try it, orca simply opens up an editor (with info on the msm I specified) and then does nothing. I've tried a variety of command lines, usually starting with this:
orca -x theDirectory theModule.msm
I use "theDirectory" as whatever empty folder I want. Like I said - it didn't do anything.
Then I tried msidb, where a couple of attempts I've made look like this:
msidb -d theModule.msm -w {storage}
msidb -d theModule.msm -x MergeModule.CABinet
In the first case, I don't know what to put for {storage}. In the second case, it turns out that the literal string "MergeModule.CABinet" is necessary (it is a reserved name). However, the extracted cabinet does not preserve the file hierarchy or "normal" file names; so I can't use it for my purposes.
Can someone explain what I'm doing wrong with the command line options? Is there any other tool that can do this?
You can use the decompiler tool included with WiX (called Dark) to decompile the merge module and extract the files:
dark.exe myMergeModule.msm -x "path_to_extracted_files"
The files will get extraced to the path specified in the -x parameter.
Note: The files will get extracted using the names specified in the File table of the installation database, which may not actually be the file names used when the files actually get installed. If you need extract the files using the actual file names, see my other answer to this question: Extracting files from merge module
I just had to do this by creating a blank msi and then use Orca to attempt to merge the module into my msi and then extract the files.
Create a blank .msi. I used WiX 3.6 to create the .msi and below is the minimal source. I named it "blank.msi".
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="blank" Language="1033" Version="1.0.0.0" Manufacturer="blank" UpgradeCode="298878d0-5e7b-4b2e-84f9-45bb66541b10">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<MediaTemplate />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder"/>
</Directory>
<ComponentGroup Id="ProductComponents" Directory="ProgramFilesFolder" />
<Feature Id="ProductFeature" Title="blank" Level="1">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
</Product>
</Wix>
Use Orca to extract the files from the merge module.
orca -m "myModule.msm" -f ProductFeature -x .\xdir blank.msi
The files will be extracted to the directory specified by the -x parameter (in this case .\xdir).
Note that the value for the -f parameter "ProductFeature" matches the name of the feature specified in msi file above.
The DeploymentToolsFoundation class library in WiX, has an InstallPackage class with an ExtractFiles() method that should do just what you want, but fails for Merge Modules. This appears to be a bug.
The following PowerShell script, which uses DTF to access the CAB in the mergemodule, should do what you want. Apologies if the scripting is a bit wonky, I'm new to PowerShell.
[Reflection.Assembly]::LoadFrom("[InsertPath]\Microsoft.Deployment.WindowsInstaller.dll")
function ExtractMSM([string]$file, [string]$targetDir)
{
write-host "Extracting files from merge module: "$file
if(![IO.Directory]::Exists($targetDir)) { new-item -type directory -path $targetDir }
$cabFile = join-path $targetDir "temp.cab"
if([IO.File]::Exists($cabFile)) { remove-item $cabFile }
$db = new-object Microsoft.Deployment.WindowsInstaller.DataBase($file, [Microsoft.Deployment.WindowsInstaller.DataBaseOpenMode]::ReadOnly)
$view = $db.OpenView("SELECT `Name`,`Data` FROM _Streams WHERE `Name`= 'MergeModule.CABinet'")
$view.Execute()
$record = $view.Fetch()
$record.GetStream(2, $cabFile)
$view.Dispose()
expand -F:* $cabFile $targetDir
remove-item $cabFile
$extractedFiles = get-childitem $targetDir
$hashFiles = #{}
foreach($extracted in $extractedFiles)
{
try
{
$longName = $db.ExecuteScalar("SELECT `FileName` FROM `File` WHERE `File`='{0}'", $extracted.Name)
}
catch
{
write-host "$($extracted.Name) is not in the MSM file"
}
if($longName)
{
$longName = $LongName.SubString($LongName.IndexOf("|") + 1)
Write-host $longName
#There are duplicates in the
if($hashFiles.Contains($longName))
{
write-host "Removing duplicate of $longName"
remove-item $extracted.FullName
}
else
{
write-host "Rename $($extracted.Name) to $longName"
$hashFiles[$longName] = $extracted
$targetFilePath = join-path $targetDir $longName
if([IO.File]::Exists($targetFilePath)) {remove-item $targetFilePath}
rename-item $extracted.FullName -NewName $longName
}
}
}
$db.Dispose()
}
I had a similar problem, but I went at it from a different direction.
I installed InstallSheild Express that came with an earlier version of Visual Studio, created a new project, but I only added the MSM file that I required.
After compiling and running my new install I was able to retrieve the files that the MSM file contained.
MSI2XML

Resources