解决方案级 --output 选项不再对生成相关命令有效

在 7.0.200 SDK 中,存在一处变更:将解决方案文件与以下命令一起使用时,不再接受 --output/-o 选项:

  • build
  • clean
  • pack
  • publish
  • store
  • test
  • vstest

这是因为 OutputPath 属性的语义(由 --output/-o 选项控制)没有针对解决方案明确定义。 以这种方式生成的项目会将其输出放置在同一目录下,这是不一致的,会导致很多用户报告的问题。

在 7.0.201 SDK 中,此变更的严重性级别已降低到警告等级,并且 pack 已从受影响的命令列表中删除。

引入的版本

.NET 7.0.200 SDK,仅在 7.0.201 SDK 中降低到警告等级。

旧行为

以前,如果在使用解决方案文件时指定了 --output/-o,所有生成项目的输出将按未定义且不一致的顺序放置在指定目录下。

新行为

如果将 --output/-o 选项与解决方案文件一起使用,dotnet CLI 将出错。 从 7.0.201 SDK 开始,将改为发出警告,如果是 dotnet pack,则不会生成警告或错误。

中断性变更的类型

此中断性变更可能需要修改才能生成脚本和持续集成管道。 因此,它会影响二进制和源兼容性。

更改原因

进行此更改是因为 OutputPath 属性(由 --output/-o 选项控制)的语义未针对解决方案明确定义。 以这种方式生成的项目会将其输出放置在同一目录下,这是不一致的,会导致很多用户报告的问题。

使用 --output 选项生成解决方案时,所有项目的 OutputPath 属性被设置为相同的值,这意味着所有项目都会将其输出将放置在同一目录下。 根据解决方案中项目的复杂性,可能会出现不同、不一致的结果。 让我们来看看不同解决方案形状的一些示例,以及它们是如何被共享的 OutputPath 所影响的。

单个项目,单个 TargetFramework

假设有一个解决方案包含一个面向单个 TargetFrameworknet7.0 的项目。 在这种情况下,提供 --output 选项等效于在项目文件中设置 OutputPath 属性。 在生成(或其他命令,但让我们暂时将讨论范围限定为生成)期间,项目的所有输出都将放置在指定的目录下。

单个项目、多个 TargetFramework

现在假设有一个解决方案包含一个具有多个 TargetFrameworksnet6.0net7.0 的项目。 由于是多目标,项目将生成两次,一次用于一个 TargetFramework。 对于每个“内部”生成,OutputPath 将被设置为相同的值,因此每个内部生成的输出将放在同一目录下。 这意味着,无论哪个生成最后完成,都将覆盖另一个生成的输出,并且在默认情况下运行的 MSBuild 等并行生成系统中,“最后”是不确定的。

库 => 控制台 => 测试,单个 TargetFramework

现在,假设有一个解决方案包含一个库项目、一个引用了库项目的控制台项目,以及一个引用了控制台项目的测试项目。 所有这些项目都以单个 TargetFrameworknet7.0 为目标。 在这种情况下,将首先生成库项目,然后生成控制台项目。 测试项目将最后生成,并将引用控制台项目。 对于每个生成的项目,每个生成的输出都将复制到 OutputPath 指定的目录,因此最终目录将包含所有三个项目的资产。 这适用于测试,但对于发布来说,这可能会导致测试资产被发送到生产环境。

库 => 控制台 => 测试,多个 TargetFramework

现在,采用相同的项目链,除了 net7.0 生成之外,还向其添加 net6.0 TargetFramework 生成。 发生与单项目、多目标生成相同的问题 - 将特定于 TFM 的资产复制到指定目录时出现不一致的情况。

多个应用

到目前为止,我们一直在研究具有线性依赖项关系图的方案,但许多解决方案可能包含多个相关应用程序。 这意味着多个应用可以同时生成到同一输出文件夹。 如果应用包含同名的依赖项文件,则当多个项目同时尝试写入输出路径中的该文件时,生成可能出现间歇性失败。

如果多个应用依赖于不同版本的文件,那么即使生成成功,将哪个版本的文件复制到输出路径也可能是不确定的。 当项目(可能以可传递方式)依赖于不同版本的 NuGet 包时,可能会发生这种情况。 在单个项目中,NuGet 有助于确保其依赖项(包括通过 NuGet 包和/或项目引用的任何可传递依赖项)都统一到同一版本。 由于统一是在单个项目及其依赖项目的上下文中实现的,这意味着在生成两个单独的顶级项目时,可以解析不同版本的包。 如果依赖于较高版本的项目最后才复制依赖项,应用往往会成功运行。 但是,如果最后复制较低版本,针对较高版本编译的应用将无法在运行时加载程序集。 由于复制的版本可能是不确定的,这可能会导致零星的、不可靠的内部版本,在这种情况下很难诊断出问题。

其他示例

有关此基本错误在实践中如何呈现的更多示例,请参阅 dotnet/sdk#15607 上的讨论。

一般建议是执行之前在没有 --output/-o 选项的情况下采取的操作,然后在命令完成后将输出移动到所需位置。 也可以在特定项目中执行此操作,但仍应用 --output/-o 选项,因为它具有更加明确定义的语义。

如果要准确维护现有行为,可以使用 --property 标志将 MSBuild 属性设置为所需目录。 要使用的属性因命令而异:

命令 属性 示例
build OutputPath dotnet build --property:OutputPath=DESIRED_PATH
clean OutputPath dotnet clean --property:OutputPath=DESIRED_PATH
pack PackageOutputPath dotnet pack --property:PackageOutputPath=DESIRED_PATH
publish PublishDir dotnet publish --property:PublishDir=DESIRED_PATH
store OutputPath dotnet store --property:OutputPath=DESIRED_PATH
test TestResultsDirectory dotnet test --property:OutputPath=DESIRED_PATH

注意:为了获得最佳结果,应该将 DESIRED_PATH 作为绝对路径。 相对路径将以你可能意想不到的方式“锚定”(即成为绝对路径),而且可能不会在所有的命令中起相同的作用。