WiX 项目永远不会是最新的
WiX project is never up-to-date
我有一个使用 Wix Toolset v3.14 的安装程序 (.msi) 项目。由于某种原因,它永远不会是最新的——即再次构建它总是会产生一些 activity(C:\Program Files (x86)\WiX Toolset v3.14\bin\Light.exe
被调用,但不是 candle.exe
)。有什么办法可以找到并解决问题吗?
这是我在打开详细输出时观察到的结果:
Target "ReadPreviousBindInputsAndBuiltOutputs" in file "C:\Program Files (x86)\MSBuild\Microsoft\WiX\v3.x\wix.targets" from project "<my-project>" (target "Link" depends on it):
Task "ReadLinesFromFile"
Task Parameter:File=obj\x64\Debug\<my-project>.wixproj.BindContentsFileListen-us.txt
Output Item(s):
_BindInputs=
C:\Users\<me>\AppData\Local\Temp\a5uljxg1\MergeId.418703\api_ms_win_core_console_l1_1_0.dll.AF4EABEE_4589_3789_BA0A_C83A71662E1D
...
Done building target "ReadPreviousBindInputsAndBuiltOutputs" in project "<my-project>.wixproj".
Target "Link" in file "C:\Program Files (x86)\MSBuild\Microsoft\WiX\v3.x\wix.targets" from project "<my-project>.wixproj" (target "CompileAndLink" depends on it):
Building target "Link" completely.
Input file "C:\Users\<me>\AppData\Local\Temp\a5uljxg1\MergeId.418703\api_ms_win_core_console_l1_1_0.dll.AF4EABEE_4589_3789_BA0A_C83A71662E1D" does not exist.
...
<and here it executes Light.exe>
因此,它看起来像 BindContentsFileListen-us.txt
并期望它包含在上次构建 运行 期间输入的文件。但是,不幸的是,其中一些文件是在临时文件夹中生成并被清除(大概是在上次构建期间)并且由于它们不再存在 - Link
步骤被重新执行。我每次按 F7 时都会观察到这种模式,只有 MergeId.418703
中的数字每次都会改变(对我来说看起来像进程 ID)。
更新:这是一个已知的(而且相当古老)issue。截至目前,它计划在 WiX v4.0 中修复。
我遇到了同样的问题,除了这个问题,我找到的唯一信息是 2013 年的一个非常无用的邮件线程(1, 2) and an issue 来自同一时代。
疑难解答
查看日志和Wix的源码发现bug如下:
light.exe
,链接器,接收它应该合并的所有对象 (.wixobj
) 文件,其中一些引用 .msm
合并模块的文件路径.
light.exe
使用组合mergemod.dll
's IMsmMerge::ExtractCAB
and cabinet.dll
's ::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%
.[=49= 下的任何内容]
<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;
}
}
}
好了,正在对合并模块进行最新检测。
我有一个使用 Wix Toolset v3.14 的安装程序 (.msi) 项目。由于某种原因,它永远不会是最新的——即再次构建它总是会产生一些 activity(C:\Program Files (x86)\WiX Toolset v3.14\bin\Light.exe
被调用,但不是 candle.exe
)。有什么办法可以找到并解决问题吗?
这是我在打开详细输出时观察到的结果:
Target "ReadPreviousBindInputsAndBuiltOutputs" in file "C:\Program Files (x86)\MSBuild\Microsoft\WiX\v3.x\wix.targets" from project "<my-project>" (target "Link" depends on it):
Task "ReadLinesFromFile"
Task Parameter:File=obj\x64\Debug\<my-project>.wixproj.BindContentsFileListen-us.txt
Output Item(s):
_BindInputs=
C:\Users\<me>\AppData\Local\Temp\a5uljxg1\MergeId.418703\api_ms_win_core_console_l1_1_0.dll.AF4EABEE_4589_3789_BA0A_C83A71662E1D
...
Done building target "ReadPreviousBindInputsAndBuiltOutputs" in project "<my-project>.wixproj".
Target "Link" in file "C:\Program Files (x86)\MSBuild\Microsoft\WiX\v3.x\wix.targets" from project "<my-project>.wixproj" (target "CompileAndLink" depends on it):
Building target "Link" completely.
Input file "C:\Users\<me>\AppData\Local\Temp\a5uljxg1\MergeId.418703\api_ms_win_core_console_l1_1_0.dll.AF4EABEE_4589_3789_BA0A_C83A71662E1D" does not exist.
...
<and here it executes Light.exe>
因此,它看起来像 BindContentsFileListen-us.txt
并期望它包含在上次构建 运行 期间输入的文件。但是,不幸的是,其中一些文件是在临时文件夹中生成并被清除(大概是在上次构建期间)并且由于它们不再存在 - Link
步骤被重新执行。我每次按 F7 时都会观察到这种模式,只有 MergeId.418703
中的数字每次都会改变(对我来说看起来像进程 ID)。
更新:这是一个已知的(而且相当古老)issue。截至目前,它计划在 WiX v4.0 中修复。
我遇到了同样的问题,除了这个问题,我找到的唯一信息是 2013 年的一个非常无用的邮件线程(1, 2) and an issue 来自同一时代。
疑难解答
查看日志和Wix的源码发现bug如下:
light.exe
,链接器,接收它应该合并的所有对象 (.wixobj
) 文件,其中一些引用.msm
合并模块的文件路径.light.exe
使用组合mergemod.dll
'sIMsmMerge::ExtractCAB
andcabinet.dll
's::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%
.[=49= 下的任何内容]
<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;
}
}
}
好了,正在对合并模块进行最新检测。