排查程序集引用问题
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
它不会执行实际复制(稍后在实际编译步骤后进行处理),但它准备要复制的文件项列表。
ResolveAssemblyReference
从 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
全面记录其输入:
Parameters
节点是所有任务的标准,但除此之外,ResolveAssemblyReference
在输入下记录其自己的信息集(这基本上与Parameters
下面的信息相同,但结构不同)。
最重要的输入为Assemblies
和AssemblyFiles
:
<ResolveAssemblyReference
Assemblies="@(Reference)"
AssemblyFiles="@(_ResolvedProjectReferencePaths);@(_ExplicitReference)"
Assemblies
在ResolveAssemblyReference
为项目调用时,使用Reference
MSBuild 项的内容。 所有元数据和程序集引用(包括 NuGet 引用)都应包含在此项中。 每个引用都附加了一组丰富的元数据:
AssemblyFiles
来自名为 _ResolvedProjectReferencePaths
的 ResolveProjectReference
目标输出项。 ResolveProjectReference
在 ResolveAssemblyReference
之前运行,它将<ProjectReference>
项转换为磁盘上生成的程序集的路径。 因此,AssemblyFiles
将包含由当前项目的所有引用项目生成的程序集:
另一个有用的输入是布尔 FindDependencies
参数,该参数从 _FindDependencies
属性中提取其值:
FindDependencies="$(_FindDependencies)"
您可以在生成中将此属性设置为 false
以关闭分析可传递依赖项程序集。
ResolveAssemblyReference 算法
ResolveAssemblyReference
任务的简化算法如下所示:
- 日志输入。
- 检查
MSBUILDLOGVERBOSERARSEARCHRESULTS
环境变量。 将此变量设置为任何值以获取更详细的日志。 - 初始化引用对象的表。
- 从
obj
目录读取缓存文件(如果存在)。 - 计算依赖项的关闭。
- 生成输出表。
- 将缓存文件写入
obj
目录。 - 记录结果。
该算法采用程序集的输入列表(来自元数据和项目引用),检索它处理的每个程序集(通过读取元数据)的引用列表,并生成所有引用程序集的完整集(可传递关闭),并从各种位置(包括 GAC、AssemblyFoldersEx 等)解析它们。
被引用的程序集会被反复添加到列表中,直到不再有新的引用。 然后算法停止。
您提供给任务的直接引用称为“主要引用”。 由于可传递引用而添加到集的间接程序集称为 Dependency。 每个间接程序集的记录将跟踪导致其包含及其相应元数据的所有主(“root”)项。
ResolveAssemblyReference 任务的结果
ResolveAssemblyReference
提供结果的详细日志记录:
解析的程序集分为两个类别:主要引用和依赖项。 主要引用被明确指定为正在生成的项目的引用。 依赖项从引用的引用中推断出来。
重要
ResolveAssemblyReference
读取程序集元数据以确定给定程序集的引用。 当 C# 编译器发出程序集时,它只会添加对实际需要的程序集的引用。 因此,在编译某个项目时,项目可能会指定不需要的引用,该引用不会被编入程序集中。 可以添加对不需要的项目的引用;它们将被忽略。
CopyLocal 项元数据
引用可以有 CopyLocal
元数据,也可以没有。 如果引用有 CopyLocal = true
,则稍后会被 CopyFilesToOutputDirectory
目标复制到输出目录。 在此示例中, DataFlow
将 CopyLocal
设置为 true,但 Immutable
没有:
如果完全缺少 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=true
,CopyLocal
则被设置为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
元数据添加到上面指定的项:
这简化了调查,并确切地告诉您哪个引用导致相关依赖项被设置为 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 结构化日志查看器中搜索“存在冲突”。 其中显示了有关哪些引用需要哪些版本程序集的详细信息。