我遇到了同样的问题,除了这个问题,我发现的唯一信息是 2013 年的一个非常无用的邮件线程(1、2)和同一时代的 issue。
疑难解答
通读日志和Wix的源代码,发现Bug如下:
-
light.exe,链接器,接收它应该合并的所有对象 (.wixobj) 文件,其中一些引用 .msm 合并模块的文件路径。
-
light.exe使用mergemod.dll的IMsmMerge::ExtractCAB和cabinet.dll的::FDICopy的组合(通过自己的winterop.dll)将合并模块的内容提取到临时路径:
// Binder.cs:5612, ProcessMergeModules
// extract the module cabinet, then explode all of the files to a temp directory
string moduleCabPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, safeMergeId, ".module.cab");
merge.ExtractCAB(moduleCabPath);
string mergeIdPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", safeMergeId);
Directory.CreateDirectory(mergeIdPath);
using (WixExtractCab extractCab = new WixExtractCab())
{
try
{
extractCab.Extract(moduleCabPath, mergeIdPath);
}
// [...]
}
-
同时,合并模块的内容被插入到fileRows集合中的其他输入文件中:
// Binder.cs:5517, ProcessMergeModules
// NOTE: this is very tricky - the merge module file rows are not added to the
// file table because they should not be created via idt import. Instead, these
// rows are created by merging in the actual modules
FileRow fileRow = new FileRow(null, this.core.TableDefinitions["File"]);
// [...]
fileRow.Source = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", wixMergeRow.Number.ToString(CultureInfo.InvariantCulture.NumberFormat), Path.DirectorySeparatorChar, record[1]);
FileRow collidingFileRow = fileRows[fileRow.File];
FileRow collidingModuleFileRow = (FileRow)uniqueModuleFileIdentifiers[fileRow.File];
if (null == collidingFileRow && null == collidingModuleFileRow)
{
fileRows.Add(fileRow);
// keep track of file identifiers in this merge module
uniqueModuleFileIdentifiers.Add(fileRow.File, fileRow);
}
// [...]
-
fileRows 最终被写入中间目录内的<project_name>BindContentsFileList<culture>.txt 文件,包括从合并模块中提取的临时(和随机命名的)文件:
// Binder.cs:7346
private void CreateContentsFile(string path, FileRowCollection fileRows)
{
string directory = Path.GetDirectoryName(path);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
using (StreamWriter contents = new StreamWriter(path, false))
{
foreach (FileRow fileRow in fileRows)
{
contents.WriteLine(fileRow.Source);
}
}
}
-
在下一次构建期间,来自wix2010.targets 的ReadPreviousBindInputsAndBuiltOutputs 目标将文件读入@(_BindInputs) 项目组。然后将此项目组列为Link 目标的输入。由于临时文件已经消失,因此目标始终被认为已过期并重新运行,从而生成一组新的临时文件,并在 BindContentsFileList 中列出,依此类推。
解决方法
一个实际的解决方法是修补 Wix,以便在 .wixobj 文件中发现的合并模块在 BindContentsFileList 中列出,而在链接期间从它们提取的文件则不在。不幸的是,我无法编译 Wix 的源代码,也懒得去完成它的分发过程。因此,这是我实施的解决方法。
从输入列表中删除临时文件
这是使用自定义目标完成的,该目标位于 ReadPreviousBindInputsAndBuiltOutputs 和 Link 之间并过滤 @(_BindInputs) 以删除 %temp% 下的任何内容。
<Target
Name="RemoveTempFilesFromBindInputs"
DependsOnTargets="ReadPreviousBindInputsAndBuiltOutputs"
BeforeTargets="Link"
>
<PropertyGroup>
<!-- This includes a final backslash, so we can use StartsWith. -->
<TemporaryDirectory>$([System.IO.Path]::GetTempPath())</TemporaryDirectory>
</PropertyGroup>
<ItemGroup>
<_BindInputs
Remove="@(_BindInputs)"
Condition="$([System.String]::new('%(FullPath)').StartsWith('$(TemporaryDirectory)'))"
/>
</ItemGroup>
</Target>
此时,Link 仅在实际输入文件更改时触发。成功!但是,不会检测到对.msm 文件的更改。无论如何,这可能是一个足够好的解决方案,因为合并模块通常是静态的。否则……
检测合并模块的更改
主要障碍是对.msm 文件的唯一引用是在.wxs 源文件中,因此我们需要弥合它与MSBuild 之间的差距。有几种方法可以使用,例如解析.wixobj 以找出WixMerge 表。但是,我已经有了代码来生成 Wix 代码,所以我这样做了,将合并模块提升到 MSBuild 项目组中,并使用自定义任务生成引用它们的 .wxs 文件一个特征。完整代码如下:
<Target
Name="GenerateMsmFragment"
BeforeTargets="GenerateCompileWithObjectPath"
Inputs="@(MsmFiles)"
Outputs="$(IntermediateOutputPath)MsmFiles.wxs"
>
<GenerateMsmFragment
MsmFiles="@(MsmFiles)"
FeatureName="MsmFiles"
MediaId="2"
OutputFile="$(IntermediateOutputPath)MsmFiles.wxs"
>
<Output TaskParameter="OutputFile" ItemName="Compile" />
</GenerateMsmFragment>
</Target>
// GenerateMsmFragment.cs
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace tasks
{
[ComVisible(false)]
public class GenerateMsmFragment : Task
{
[Required]
public ITaskItem[] MsmFiles { get; set; }
[Required]
public string FeatureName { get; set; }
[Required]
public string MediaId { get; set; }
[Output]
public ITaskItem OutputFile { get; set; }
public override bool Execute()
{
var xmlns = "http://schemas.microsoft.com/wix/2006/wi";
var outputXml = new XmlDocument();
outputXml.AppendChild(outputXml.CreateXmlDeclaration("1.0", "utf-8", null));
var fragmentElem = outputXml
.AppendElement("Wix", xmlns)
.AppendElement("Fragment", xmlns);
{
var mediaElem = fragmentElem.AppendElement("Media", xmlns);
mediaElem.SetAttribute("Id", MediaId);
mediaElem.SetAttribute("Cabinet", "MsmFiles.cab");
mediaElem.SetAttribute("EmbedCab", "yes");
}
{
var directoryRefElem = fragmentElem.AppendElement("DirectoryRef", xmlns);
directoryRefElem.SetAttribute("Id", "TARGETDIR");
var featureElem = fragmentElem.AppendElement("Feature", xmlns);
featureElem.SetAttribute("Id", FeatureName);
featureElem.SetAttribute("Title", "Imported MSM files");
featureElem.SetAttribute("AllowAdvertise", "no");
featureElem.SetAttribute("Display", "hidden");
featureElem.SetAttribute("Level", "1");
foreach (var msmFilePath in MsmFiles.Select(i => i.ItemSpec)) {
var mergeElem = directoryRefElem.AppendElement("Merge", xmlns);
mergeElem.SetAttribute("Id", msmFilePath);
mergeElem.SetAttribute("SourceFile", msmFilePath);
mergeElem.SetAttribute("DiskId", MediaId);
mergeElem.SetAttribute("Language", "0");
featureElem
.AppendElement("MergeRef", xmlns)
.SetAttribute("Id", msmFilePath);
}
}
Directory.CreateDirectory(Path.GetDirectoryName(OutputFile.GetMetadata("FullPath")));
outputXml.Save(OutputFile.GetMetadata("FullPath"));
return true;
}
}
}
// XmlExt.cs
using System.Xml;
namespace nrm
{
public static class XmlExt
{
public static XmlElement AppendElement(this XmlDocument element, string qualifiedName, string namespaceURI)
{
var newElement = element.CreateElement(qualifiedName, namespaceURI);
element.AppendChild(newElement);
return newElement;
}
public static XmlElement AppendElement(this XmlNode element, string qualifiedName, string namespaceURI)
{
var newElement = element.OwnerDocument.CreateElement(qualifiedName, namespaceURI);
element.AppendChild(newElement);
return newElement;
}
}
}
瞧,对合并模块进行最新检测。