【问题标题】:WiX project is never up-to-dateWiX 项目永远不会是最新的
【发布时间】:2019-08-23 18:01:23
【问题描述】:

我有一个使用 Wix Toolset v3.14 的安装程序 (.msi) 项目。由于某种原因,它永远不会是最新的——即再次构建它总是会产生一些活动(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 中修复。

【问题讨论】:

  • 如果您将构建日志提高到诊断日志级别(工具 > 选项 > 项目和解决方案 > 构建和运行 > MSBuild 项目构建输出详细程度),它通常具有将项目构建为构建解决方案时的第一个日志行。确保您在生成任何文件时都在您的 msbuild 目标上使用了适当的输入/输出属性。
  • @BrianSutherland 我已经用其他观察更新了我的问题......
  • 是的,如果输出不存在、输入不存在或任何输入修改日期比任何输出修改日期新,则 MSBuild 将运行目标。看起来您缺少该临时文件,因此它将重建。不幸的是,我不知道它来自哪里,也不知道为什么每次运行它都会被删除。您可能必须深入研究 wix.targets 或慢慢阅读 msbuild 输出才能弄清楚。当我再次构建安装程序项目时,我只会得到Skipping target "Link" because all output files are up-to-date with respect to the input files.
  • 我打赌这是因为我的.wxs 合并到了Microsoft_VC140_CRT_x64.msm...唉,我的 WiX 知识非常基础

标签: visual-studio wix windows-installer


【解决方案1】:

我遇到了同样的问题,除了这个问题,我发现的唯一信息是 2013 年的一个非常无用的邮件线程(12)和同一时代的 issue

疑难解答

通读日志和Wix的源代码,发现Bug如下:

  • light.exe,链接器,接收它应该合并的所有对象 (.wixobj) 文件,其中一些引用 .msm 合并模块的文件路径。

  • light.exe使用mergemod.dllIMsmMerge::ExtractCABcabinet.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 最终被写入中间目录内的&lt;project_name&gt;BindContentsFileList&lt;culture&gt;.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.targetsReadPreviousBindInputsAndBuiltOutputs 目标将文件读入@(_BindInputs) 项目组。然后将此项目组列为Link 目标的输入。由于临时文件已经消失,因此目标始终被认为已过期并重新运行,从而生成一组新的临时文件,并在 BindContentsFileList 中列出,依此类推。

解决方法

一个实际的解决方法是修补 Wix,以便在 .wixobj 文件中发现的合并模块在 BindContentsFileList 中列出,而在链接期间从它们提取的文件则不在。不幸的是,我无法编译 Wix 的源代码,也懒得去完成它的分发过程。因此,这是我实施的解决方法。

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

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

瞧,对合并模块进行最新检测。

【讨论】:

  • 写得真好!我会检查它是否对我有用,并会回复你。谢谢!
  • 它可以工作,尽管我必须指出,使用解决方法构建仍然会被执行(即它不是最新的),但它会跳过light.exe 步骤(因为我们修改输入列表作为一部分的构建)。谢谢你,@Quentin。
  • @C.M.我一直在玩一个微不足道的 Wix 项目,看起来 Wix VS 扩展根本没有 快速更新检查。搜索源代码没有产生任何结果,所以我倾向于认为这是我们将得到的最好的结果。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多