在 MSBuild 和 .NET 构建过程中,最重要的任务之一是在 ResolveAssemblyReference 任务中解析程序集引用。 本文解释了如何 ResolveAssemblyReference 工作的一些详细信息,以及在 ResolveAssemblyReference 无法解决引用时如何排查和解决可能发生的生成失败。 若要调查程序集引用失败,可能需要安装 结构化日志查看器 以查看 MSBuild 日志。 本文中的屏幕截图取自结构化日志查看器。
目的是通过.csproj文件(或其他位置)中指定的<Reference>项,获取所有引用,并将其映射为文件系统中的程序集文件路径。
编译器只能接受文件系统上的一个 .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 由 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 目标。 此链接准确显示了在.targets文件中调用ResolveAssemblyReference任务的位置。
ResolveAssemblyReference 参数输入
ResolveAssemblyReference 对输入的记录非常全面:
节点 Parameters 是所有任务的标准配置,但此外,节点 ResolveAssemblyReference 会在“输入”下记录自己的信息集(这基本上与 Parameters 下的信息相同,但结构有所不同)。
最重要的输入是Assemblies和AssemblyFiles。
<ResolveAssemblyReference
Assemblies="@(Reference)"
AssemblyFiles="@(_ResolvedProjectReferencePaths);@(_ExplicitReference)"
Assemblies使用 MSBuild 项Reference的内容,在ResolveAssemblyReference为项目调用时。 所有元数据和程序集引用(包括 NuGet 引用)都应包含在此项中。 每个引用都附加了一组丰富的元数据:
AssemblyFiles 来自 ResolveProjectReference 名为 _ResolvedProjectReferencePaths 的目标输出项。
ResolveProjectReference 在 ResolveAssemblyReference 之前运行,它将 <ProjectReference> 项转换为磁盘上生成的程序集路径。 因此,AssemblyFiles 将包含由当前项目所有引用项目生成的程序集:
另一个有用的输入是布尔 FindDependencies 参数,其值取自 _FindDependencies 属性。
FindDependencies="$(_FindDependencies)"
在生成中,可以通过将此属性设置为 false 来关闭对可传递依赖项程序集的分析。
ResolveAssemblyReference 算法
简化的算法用于 ResolveAssemblyReference 任务如下所示:
- 日志输入。
- 检查
MSBUILDLOGVERBOSERARSEARCHRESULTS环境变量。 将此变量设置为任何值以获取更详细的日志。 - 初始化引用表对象。
- 从
obj目录读取缓存文件(如果存在)。 - 计算依赖项的关闭。
- 生成输出表。
- 将缓存文件写入
obj目录。 - 记录结果。
该算法读取程序集的输入列表(包括来自元数据和项目引用的),获取每个处理的程序集的引用列表(通过读取元数据),并建立所有引用程序集的全集(传递闭包),从各种位置解析这些引用,包括 GAC、AssemblyFoldersEx 等。
当没有更多新引用被添加时,引用程序集将以迭代方式添加到列表中。 然后算法停止。
你提供给任务的直接引用称为“主要引用”。 由于传递引用而添加到集合中的间接程序集称为“Dependency”。 每个间接程序集的记录都会记录所有导致其包含的主要(“根”)项及其对应的元数据。
ResolveAssemblyReference 任务的结果
ResolveAssemblyReference 提供结果的详细日志记录:
已解析的程序集分为两个类别:主引用和依赖项。 主要参考被显式指定为正在构建的项目中的参考。 通过传递方式从引用的引用中推断出依赖项。
重要
ResolveAssemblyReference 读取程序集元数据以确定给定程序集的引用。 当 C# 编译器发出程序集时,它只会添加对实际需要的程序集的引用。 因此,在将某个项目编译成程序集时,该项目可能会指定一个不必要的引用,该引用不会嵌入到程序集的构建中。 可以添加对不需要的项目的引用;它们将被忽略。
CopyLocal 项元数据
引用也可以具有 CopyLocal 元数据,也可以没有。 如果引用具有 CopyLocal = true,那么稍后由CopyFilesToOutputDirectory目标任务复制到输出目录。 在此示例中,DataFlow 将CopyLocal 设置为 true,而 Immutable 则未设置为 true:
如果CopyLocal元数据完全丢失,则默认假设其为 true。 因此 ResolveAssemblyReference ,默认情况下会尝试将依赖项复制到输出,除非它找到不这样做的原因。
ResolveAssemblyReference 记录了它选择某个引用作为 CopyLocal 或不选择的原因。
下表列举了决策的所有可能原因 CopyLocal 。 知道这些字符串,以便在生成日志中搜索它们,这非常有用。
| CopyLocal 状态 | Description |
|---|---|
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 所有主要引用上的元数据。 每个引用(主要或依赖项)都有一个包含所有导致该引用被加入封闭 (closure) 的主要引用(源项)的列表。
- 如果未指定任何源项指定
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 在第一个位置找到后会停止。
AssemblySearchPaths 是以分号分隔的列表。 它支持一组内置占位符(例如, {HintPathFromItem} 以及 {GAC})在解析过程中扩展到实际位置。
默认情况下,非 SDK 样式项目使用以下搜索路径顺序:
- 候选程序集文件 (
{CandidateAssemblyFiles}) - 属性
ReferencePath($(ReferencePath)) - 来自
<Reference>项目的提示路径({HintPathFromItem}) - 目标框架目录 (
{TargetFrameworkDirectory}) - 来自
AssemblyFolders.config($(AssemblyFoldersConfigFileSearchPath)) 的程序集文件夹 - 注册表 (
{Registry:...}) - 旧版注册的程序集文件夹 (
{AssemblyFolders}) - 全局程序集缓存 (GAC) (
{GAC}) - 将
<Reference Include="...">值视为真实文件名 ({RawFileName}) - 输出目录 (
$(OutDir))
.NET SDK 设置较小的默认值 AssemblySearchPaths (默认情况下不包括 GAC/注册表/输出目录搜索):
{CandidateAssemblyFiles}{HintPathFromItem}{TargetFrameworkDirectory}{RawFileName}
若要查看生成的有效值,请检查由
可以通过将每个条目的相关标志设置为false来禁用这些条目。
- 通过将
AssemblySearchPath_UseCandidateAssemblyFiles属性设置为 false 来禁用从当前项目中搜索文件。 - 通过将
AssemblySearchPath_UseReferencePath属性设置为 false 来禁用(从.user文件)搜索引用路径属性。 - 将
AssemblySearchPath_UseHintPathFromItem属性设置为 false 可以禁用项中的提示路径。 - 通过将
AssemblySearchPath_UseTargetFrameworkDirectory属性设置为 false,可以禁用目录与 MSBuild 目标运行时的结合使用。 - 通过将
AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath属性设置为false来禁用从AssemblyFolders.config搜索程序集文件夹。 - 通过将属性设置为
AssemblySearchPath_UseRegistryfalse 来禁用搜索注册表。 - 通过将属性设置为
AssemblySearchPath_UseAssemblyFoldersfalse 来禁用搜索旧注册的程序集文件夹。 - 通过将
AssemblySearchPath_UseGAC属性设置为 false 来禁用查找 GAC。 - 要禁用将引用的 Include 视为真实文件名,可以将
AssemblySearchPath_UseRawFileName属性设置为 false。 - 通过将
AssemblySearchPath_UseOutDir属性设置为 false 来禁用检查应用程序的输出文件夹。
发生了冲突
常见情况是 MSBuild 发出警告,指出不同引用使用了同一程序集的不同版本。 该解决方案通常涉及将绑定重定向添加到 app.config 文件。
调查这些冲突的一种有用方法是在 MSBuild 结构化日志查看器中搜索“存在冲突”。 其中显示了有关哪些引用需要哪些版本的程序集的详细信息。