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 的源代码,也懒得去完成它的分发过程。因此,这是我实施的解决方法。

正在从输入列表中删除临时文件

这是使用自定义目标完成的,该目标位于 ReadPreviousBindInputsAndBuiltOutputsLink 之间并过滤 @(_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;
        }
    }
}

好了,正在对合并模块进行最新检测。