相关文章推荐
爱看球的太阳  ·  Android ...·  9 月前    · 

处理不会每次都发生的生成失败是让人沮丧的体验。 本文将帮助你确定根本原因并做出更改,使你能够修复间歇性生成失败,以便生成每次都一致地运行。

MSBuild 支持并行生成,方式是在不同的 CPU 核心上运行不同的工作器节点进程。 尽管并行生成通常具有显著的性能优势,但这样做也可能会带来在多个进程尝试同时使用同一资源时发生错误的风险。 这种情况是一种争用条件。 争用条件可以显示不同生成之间的不同行为。 例如,一个进程可能会因时间的不同而领先或落后于另一个进程。

因文件 I/O 争用引起的错误消息始终包括操作系统文件 I/O 失败,但可能包含不同的 MSBuild 错误代码,具体取决于发生文件 I/O 错误时生成中所发生的情况。 在 Windows 平台上,一些示例可能如下所示:

error MSB3677: Unable to move file "source" to "dest".
Cannot create a file when that file already exists. [{project file}] 
The process cannot access the file 'file' because it is being used by another process.

当系统要求使用多个属性设置组合生成特定项目时,可能会发生文件争用。 每当属性设置发生更改时,MSBuild 通常都会为引用的项目执行单独的生成,在此情况下,输出可能也有所不同。 如果文件已位于同一位置,或者由于目标文件正被另一个 MSBuild 进程使用,则移动或复制操作可能会失败,具体取决于并发运行生成的时间。 此外,如果另一个 MSBuild 进程正在读取或写入同一文件,则文件读取操作可能会失败。

你可以通过了解原因并对项目文件进行适当的更改来永久修复大多数生成文件争用问题,但前提是问题出现在你自己的代码中。 争用条件也可能是由 SDK 代码中的 bug 引起的,在这种情况下,必须将问题报告给相关 SDK 的所有者并由其进行调查。

生成争用条件的原因

本部分介绍可能导致争用条件的不同类型的问题。 下一部分“诊断和修复争用条件”介绍如何解决这些问题。

ProjectReference 属性设置不一致

同一项目的不同生成是许多生成过程的正常部分;当 MSBuild 在多个设置组合下生成输出时,会产生这些生成。 例如,一个解决方案可能有多个目标框架(如 net472net7)或多个目标平台体系结构(如 Arm64x64)。 通过为每个输出组合指定不同的输出文件夹,可满足此生成要求。 这样一来,Arm64net472版本的程序集会输出到与其他组合不同的文件夹,并且不会发生冲突。 默认 SDK 设置已处理此处提到的示例,但有时,多个设置组合的发生并不那么明显,需要进行调查。

ProjectReference 属性与全局属性冲突

全局属性(即使用 /p/property 选项在命令行上设置的属性)隐式用于引用的项目生成。 但是,通过使用 RemoveGlobalPropertiesGlobalPropertiesToRemove,可以省略任何给定项目引用的部分或所有全局属性设置,因此,如果这些属性的使用不一致,则在已设置全局属性时可能会生成多个版本的引用的项目,在未设置全局属性或具有其他值时可能出现另一种情况。

意外打包触发项目生成

如果生成打包之前生成的项目的输出,则当打包生成逻辑指定的属性设置与生成原始项目时所使用的属性设置不同时,可能会出现争用条件。 在这种情况下,由于属性不匹配,MSBuild 通常会触发这些项目的重新生成。 这种情况下会导致争用条件。 请考虑在打包项目中将 BuildProjectReferences 设置为 false,这样就永远不会要求生成正在打包的项目。 这意味着,仅当之前已完成项目生成并保持最新时,才应请求打包生成。

诊断和修复争用条件

当针对生成所生成的文件的移动操作、复制操作或文件写入间歇性失败时,你应该怀疑出现争用条件错误。

解决此问题的方法取决于期望的结果。 是否确实需要两个不同的生成项目版本? 如果是,请为两种不同的属性配置设置不同的输出文件夹。 否则,可以更改 ProjectReference 元素,以确保为每个引用设置相同的属性。

若要诊断和修复争用条件,请执行以下步骤。

  • 检查是否没有运行正在使用这些文件的任何其他程序,例如同一计算机上的 Visual Studio 调试器会话。

  • 如果使用 MSBuild /m:1 选项运行生成,请查明问题是否已解决。 请参阅 MSBuild 命令行参考。 这是命令行选项,用于告知 MSBuild 要用于生成的节点数。 如果设置为 1,则生成会以串行方式继续进行,并且不会发生争用条件。 使用 /m:1 选项是一种解决方法,可用于避免争用条件,但它不是长期解决方案。 生成输出的生成次数仍然不止一次,并且可能存在差异,这是一种错误情况,称为“过度生成”。 此外,串行生成会显著增加完成生成所需的时长。 如果生成在已启用并行生成时仅出现了间歇性文件 I/O 失败(处理器数目大于 1),则这是生成争用条件。

  • 生成日志。 在详细程度为 Normal 或更高程度(例如使用命名行选项 -verbosity:Normal)的情况下运行生成。 若要对争用条件进行故障排除,建议生成二进制日志(使用 /bl/binlog 选项),并使用结构化日志查看器查看该日志。 若要获取有助于诊断争用条件的日志,则不要求日志来自失败的运行,因为你仍然可以找到访问生成错误的输出的多个位置。

  • 无论该特定运行是否失败,请打开日志(或 .binlog 文件),然后搜索触发失败的文件的文件名,并查找使用该文件的所有位置。

    以下屏幕截图显示了结构化日志查看器正在查看通过构建示例中的解决方案生成的日志。 所显示的内容是文件“net5.0\Base.dll”的搜索结果,错误消息中提到了该文件。 同一输出文件在搜索结果中作为 Csc 任务的 OutputAssembly 显示了两次,说明它的生成次数不止一次。

  • 记下对该项目生成的每个实例均有效的属性设置。 结构化日志查看器可以简化此操作,因为每个项目生成都有一个“属性”节点,其中列出了对该项目生成有效的所有属性设置。 如果使用的是文本日志,则当详细程度设置为 Normal 或更高程度时,为生成设置的属性将以文本形式输出。 比较生成失败输出的项目的每个生成的属性列表。 如果问题实际上是争用条件,则应看到差异。

    以下屏幕截图显示了结构化日志查看器,其中展开了一个项目生成的“属性”节点。 请注意,此生成位于另一个项目的 ProjectReferences 节点下,这意味着此生成是由另一个项目中的 ProjectReference 元素触发的。 如果沿着树的节点往上查看,可以看到引用此生成的项目。

    将此列表与同一项目的另一个生成进行比较,可以看到缺少了 SpecialMode。 此生成是一个顶级生成,这意味着其生成原因是它在解决方案本身中,而不是因为另一个项目对它进行了引用。

  • 在项目文件中搜索 ProjectReference,其中 Include 属性指定该项目。 查找任何元数据 SetConfigurationSetPlatformSetTargetFrameworkAdditionalPropertiesRemoveGlobalPropertiesGlobalPropertiesToRemove。 查看在解决方案中不同 ProjectReference 元素之间设置的值之间的差异。 在示例中,AdditionalProperties 元数据设置不一致(出现在一个位置,但未出现在另一个位置)是问题的根源。

  • 思考不同属性设置的含义,以及它们是否会对生成输出产生重大影响。 如果属性设置差异是有意义的,并且输出应该是不同的,则解决方法是为设置的每个变体使用不同的文件夹,就像 .NET SDK 针对平台、配置(例如调试或发布)或目标框架所执行的操作一样。 如果属性设置差异是无意形成或无关紧要的,请尝试找到一种方法来更改项目代码以消除属性的差异。 在示例中,可通过将 AdditionalProperties="SpecialMode=true" 元数据添加到“Middle2.csproj”中的 ProjectReference 或通过移除“Middle1.csproj”中的 AdditionalProperties 元数据来实现此目的。 适当的更改取决于应用程序、服务或目标的具体要求。

  • 如果错误位于不由你控制的 SDK 中,请将问题报告给 SDK 所有者。

    此处提供了展示模式的简单案例。 假设你有一个解决方案,其中包含多个项目,一个前端客户端(App)、两个类库(Middle1Middle2)和一个由这两个类库引用的库(Base)。

    以下代码部分中的项目文件都是单个解决方案的一部分。 此项目集合生成了两个不同的 Base 生成,其中一个生成具有 SpecialMode=true,而另一个生成没有。 可能会出现引用 Base.dll 输出的间接性错误。 有时可能会收到错误:无法写入 Base.dll,“因为它正在被另一个进程使用”。

    <!-- Base.csproj -->
    <Project Sdk="Microsoft.NET.Sdk">
      <ItemGroup>
        <ProjectReference Include="..\Base\Base.csproj" />
      </ItemGroup>
      <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
      </PropertyGroup>
    </Project>
    
    <!-- Middle1.csproj -->
    <Project Sdk="Microsoft.NET.Sdk">
      <ItemGroup>
        <ProjectReference Include="..\Base\Base.csproj" AdditionalProperties="SpecialMode=true" />
      </ItemGroup>
      <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
      </PropertyGroup>
    </Project>
    
    <!-- Middle2.csproj -->
    <Project Sdk="Microsoft.NET.Sdk">
      <ItemGroup>
        <ProjectReference Include="..\Base\Base.csproj" />
      </ItemGroup>
      <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
      </PropertyGroup>
    </Project>
    
    <!-- App.csproj -->
    <Project Sdk="Microsoft.NET.Sdk">
      <ItemGroup>
        <ProjectReference Include="..\Middle1\Middle1.csproj" />
        <ProjectReference Include="..\Middle2\Middle2.csproj" />
      </ItemGroup>
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
      </PropertyGroup>
    </Project>
    

    如果期望的行为是使用 SpecialMode,则合适的解决方法是将相同的 AdditionalProperties 元数据值添加到“Middle2.csproj”中的 ProjectReference

    <!-- Middle2.csproj -->
    <Project Sdk="Microsoft.NET.Sdk">
      <ItemGroup>
        <ProjectReference Include="..\Base\Base.csproj" AdditionalProperties="SpecialMode=true" />
      </ItemGroup>
      <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
      </PropertyGroup>
    </Project>
    

    如果符合意图,还可以通过移除“Middle1.csproj”中的 AdditionalProperties 元数据来解决生成问题。

  • 用 MSBuild 并行生成多个项目

  • MSBuild 如何生成项目

  •