排查程序集引用问题

MSBuild 和 .NET 构建过程中最重要的任务之一是解析任务 ResolveAssemblyReference 中发生的程序集引用。 本文介绍了 ResolveAssemblyReference 工作原理的一些详细信息,以及如何排查 ResolveAssemblyReference 无法解决引用时可能发生的构建失败。 若要调查程序集引用失败,可能需要安装结构化日志查看器以查看 MSBuild 日志。 本文中的屏幕截图取自结构化日志查看器。

ResolveAssemblyReference的目的是通过<Reference>项获取.csproj文件(或其他位置)中指定的所有引用,并将其映射到文件系统中的程序集文件的路径。

编译器只能接受文件系统上的一个 .dll 路径作为引用,因此 ResolveAssemblyReference 将类似 mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 在项目文件中显示的字符串转换为类似 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.dll路径,然后通过 /r 开关传递给编译器。

此外,ResolveAssemblyReference还以递归方式确定所有和应用的完整集(实际是图形理论术语中的可传递关闭),并且对于每个集合,确定是否应将其复制到生成输出目录.dll.exe 它不会执行实际复制(稍后在实际编译步骤后进行处理),但它准备要复制的文件项列表。

ResolveAssemblyReferenceResolveAssemblyReferences 目标中调用:

显示构建过程中调用 ResolveAssemblyReferences 时的日志查看器的屏幕截图。

如果您注意一下排序,ResolveAssemblyReferences 发生在之前 Compile,当然,CopyFilesToOutputDirectory 发生在之后 Compile

注意

ResolveAssemblyReference 任务在 MSBuild 安装文件夹中的标准.targets文件Microsoft.Common.CurrentVersion.targets中调用。 您还可以在https://github.com/dotnet/msbuild/blob/a936b97e30679dcea4d99c362efa6f732c9d3587/src/Tasks/Microsoft.Common.CurrentVersion.targets#L1991-L2140在线浏览 .NET SDK MSBuild 目标。 此链接准确显示ResolveAssemblyReference任务在.targets文件中的调用位置。

ResolveAssemblyReference 输入

ResolveAssemblyReference 全面记录其输入:

显示 ResolveAssemblyReference 任务的输入参数的屏幕截图。

Parameters 节点是所有任务的标准,但除此之外,ResolveAssemblyReference在输入下记录其自己的信息集(这基本上与Parameters下面的信息相同,但结构不同)。

最重要的输入为AssembliesAssemblyFiles

    <ResolveAssemblyReference
        Assemblies="@(Reference)"
        AssemblyFiles="@(_ResolvedProjectReferencePaths);@(_ExplicitReference)"

AssembliesResolveAssemblyReference为项目调用时,使用Reference MSBuild 项的内容。 所有元数据和程序集引用(包括 NuGet 引用)都应包含在此项中。 每个引用都附加了一组丰富的元数据:

显示程序集引用上元数据的屏幕截图。

AssemblyFiles 来自名为 _ResolvedProjectReferencePathsResolveProjectReference 目标输出项。 ResolveProjectReferenceResolveAssemblyReference 之前运行,它将<ProjectReference>项转换为磁盘上生成的程序集的路径。 因此,AssemblyFiles 将包含由当前项目的所有引用项目生成的程序集:

显示 AssemblyFiles 的屏幕截图。

另一个有用的输入是布尔 FindDependencies 参数,该参数从 _FindDependencies 属性中提取其值:

FindDependencies="$(_FindDependencies)"

您可以在生成中将此属性设置为 false 以关闭分析可传递依赖项程序集。

ResolveAssemblyReference 算法

ResolveAssemblyReference任务的简化算法如下所示:

  1. 日志输入。
  2. 检查 MSBUILDLOGVERBOSERARSEARCHRESULTS 环境变量。 将此变量设置为任何值以获取更详细的日志。
  3. 初始化引用对象的表。
  4. obj 目录读取缓存文件(如果存在)。
  5. 计算依赖项的关闭。
  6. 生成输出表。
  7. 将缓存文件写入 obj 目录。
  8. 记录结果。

该算法采用程序集的输入列表(来自元数据和项目引用),检索它处理的每个程序集(通过读取元数据)的引用列表,并生成所有引用程序集的完整集(可传递关闭),并从各种位置(包括 GAC、AssemblyFoldersEx 等)解析它们。

被引用的程序集会被反复添加到列表中,直到不再有新的引用。 然后算法停止。

您提供给任务的直接引用称为“主要引用”。 由于可传递引用而添加到集的间接程序集称为 Dependency。 每个间接程序集的记录将跟踪导致其包含及其相应元数据的所有主(“root”)项。

ResolveAssemblyReference 任务的结果

ResolveAssemblyReference 提供结果的详细日志记录:

显示结构化日志查看器中 ResolveAssemblyReference 结果的屏幕截图。

解析的程序集分为两个类别:主要引用和依赖项。 主要引用被明确指定为正在生成的项目的引用。 依赖项从引用的引用中推断出来。

重要

ResolveAssemblyReference 读取程序集元数据以确定给定程序集的引用。 当 C# 编译器发出程序集时,它只会添加对实际需要的程序集的引用。 因此,在编译某个项目时,项目可能会指定不需要的引用,该引用不会被编入程序集中。 可以添加对不需要的项目的引用;它们将被忽略。

CopyLocal 项元数据

引用可以有 CopyLocal 元数据,也可以没有。 如果引用有 CopyLocal = true,则稍后会被 CopyFilesToOutputDirectory 目标复制到输出目录。 在此示例中, DataFlowCopyLocal 设置为 true,但 Immutable 没有:

显示某些引用的 CopyLocal 设置的屏幕截图。

如果完全缺少 CopyLocal 元数据,则默认假定为 true。 因此,ResolveAssemblyReference 在默认情况下会尝试将依赖项复制到输出,除非它找到不这样做的原因。 ResolveAssemblyReference 记录它选择特定引用为 CopyLocal 或不为的原因。

下表列举了 CopyLocal 决策的所有可能原因。 了解这些字符串有助于在构建日志中搜索它们。

CopyLocal 状态 说明
Undecided 复制本地状态现已确定。
YesBecauseOfHeuristic 引用应该具有 CopyLocal='true' ,因为它不是出于任何原因而“否”。
YesBecauseReferenceItemHadMetadata 引用应具有 CopyLocal='true',因为它的源项具有 Private='true'
NoBecauseFrameworkFile 引用应具有 CopyLocal='false',因为它是一个框架文件。
NoBecausePrerequisite 引用应具有 CopyLocal='false',因为它是一个先决条件文件。
NoBecauseReferenceItemHadMetadata 引用应具有 CopyLocal='false',因为 Private 特性在项目中设置为“false”。
NoBecauseReferenceResolvedFromGAC 引用应具有 CopyLocal='false' ,因为它已从 GAC 解析。
NoBecauseReferenceFoundInGAC 传统行为,CopyLocal='false' 在 GAC 中找到程序集时(即使在其他位置解析时也是如此)。
NoBecauseConflictVictim 引用应具有 CopyLocal='false',因为它丢失了同名程序集文件之间的冲突。
NoBecauseUnresolved 未解析引用。 无法将其复制到 bin 目录,因为它未找到。
NoBecauseEmbedded 引用是嵌入的。 它不应被复制到 bin 目录,因为它不会在运行时加载。
NoBecauseParentReferencesFoundInGAC 属性 copyLocalDependenciesWhenParentReferenceInGac 设置为 false,并且已在 GAC 中找到所有父源项。
NoBecauseBadImage 不应复制提供的程序集文件,因为它是一个错误的映像,可能不是托管的,也可能根本不是程序集。

专用项元数据

确定 CopyLocal 的一个重要部分是所有主要引用上的 Private 元数据。 每个引用(主要或依赖项)都有一个列表,其中包含所有主要引用(源项)的列表,这些引用导致该引用被添加到关闭。

  • 如果没有任何源项指定 Private 元数据, CopyLocal 则被设置为 True(或不设置,默认为 True
  • 如果任一源项指定 Private=trueCopyLocal 则被设置为 True
  • 如果没有任何源程序集指定 Private=true,并且至少有一个指定 Private=false,则CopyLocal被设置为 False

哪个引用将 Private 设置为 false?

最后一个点通常是 CopyLocal 设置为 false 的原因:This reference is not "CopyLocal" because at least one source item had "Private" set to "false" and no source items had "Private" set to "true".

MSBuild 不会告诉我们哪个引用已设置为 Private false,但结构化日志查看器会将 Private 元数据添加到上面指定的项:

屏幕截图显示,在结构化日志查看器中将 Private 设置为 false。

这简化了调查,并确切地告诉您哪个引用导致相关依赖项被设置为 CopyLocal=false

全局程序集缓存

全局程序集缓存 (GAC) 在确定是否复制对输出的引用方面发挥了重要作用。 这是不幸的,因为 GAC 的内容特定于计算机,这会导致可重现的生成出现问题(其中行为因计算机状态的不同计算机而异,如 GAC)。

最近对 ResolveAssemblyReference 进行了修复,以缓解情况。 可以通过 ResolveAssemblyReference 的两个新输入来控制行为:

    CopyLocalDependenciesWhenParentReferenceInGac="$(CopyLocalDependenciesWhenParentReferenceInGac)"
    DoNotCopyLocalIfInGac="$(DoNotCopyLocalIfInGac)"

AssemblySearchPaths

尝试查找程序集时,可通过两种方法自定义 ResolveAssemblyReference 搜索的路径列表。 若要完全自定义列表,可以提前设置属性 AssemblySearchPaths 。 顺序很重要;如果程序集位于两个位置,ResolveAssemblyReference 会在第一个位置找到程序集后停止。

默认情况下,有 10 个位置 ResolveAssemblyReference 搜索(如果使用 .NET SDK,则有 4 哥),并且可以通过将相关标志设置为 false 来禁用每个位置搜索:

  • 通过将 AssemblySearchPath_UseCandidateAssemblyFiles 属性设置为 false 来禁用从当前项目中搜索文件。
  • 通过将 AssemblySearchPath_UseReferencePath 属性设置为 false 来禁用搜索引用路径属性(从 .user 文件)。
  • 通过将 AssemblySearchPath_UseHintPathFromItem 属性设置为 false 来禁用项中的提示路径。
  • 通过将 AssemblySearchPath_UseTargetFrameworkDirectory 属性设置为 false,禁止在 MSBuild 目标运行时使用该目录。
  • 通过将 AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath 属性设置为 false 来禁用从 AssemblyFolders.config 搜索程序集文件夹。
  • 通过将 AssemblySearchPath_UseRegistry 属性设置为 false 来禁用搜索注册表。
  • 通过将 AssemblySearchPath_UseAssemblyFolders 属性设置为 false 来禁用搜索旧注册的程序集文件夹。
  • 通过将 AssemblySearchPath_UseGAC 属性设置为 false 来禁用查找 GAC。
  • 通过将 AssemblySearchPath_UseRawFileName 属性设置为 false 来禁用引用的 Include 作为真实文件名。
  • 通过将 AssemblySearchPath_UseOutDir 属性设置为 false 来禁用检查应用程序的输出文件夹。

发生了冲突

常见情况是 MSBuild 提供有关不同引用使用的同一程序集的不同版本的警告。 该解决方案通常涉及将绑定重定向添加到 app.config 文件。

调查这些冲突的一种有用方法是在 MSBuild 结构化日志查看器中搜索“存在冲突”。 其中显示了有关哪些引用需要哪些版本程序集的详细信息。