取消

在制作多框架项目的 NuGet 包时应该注意的问题(buildMultiTargeting/TargetFrameworks)

制作一个 dll 引用的 NuGet 包简直是一键完成,无论是不是多框架项目;制作 dotnet-tools 也是如此。但如果需要自定义一些编译步骤,那么就需要在制作 NuGet 包时做很多的特殊处理了。

本文介绍制作适用于多框架项目的 NuGet 工具包时应该注意的问题。


背景知识

NuGet 包内的文件夹结构

回顾一下 NuGet 包的文件夹结构:

1
2
3
4
5
6
7
8
9
+ /
+ lib/
+ ref/
+ runtimes/
+ content/
+ build/
+ buildMultiTargeting/
+ buildTransitive
+ tools/

由于涉及到自定义 NuGet 包的代码都写在 build buildMultiTargeting 和 buildTransitive 中,其他都不涉及到 NuGet 包在编译期间会做的事情,另外,buildTransitive 是用来处理包传递过程中的编译过程的,所以我们本文只说也只需要说 build 和 buildMultiTargeting。

这里面的代码都是用 Target 写出来的,如果你对此不了解,建议阅读这些博客:

制作有自定义功能的 NuGet 包

我之前写过一些关于如何制作各种高级功能的 NuGet 包的博客:

按照上面的博客制作出来的 NuGet 包其实是适用于单框架项目和多框架项目的,甚至也适用于传统的非 SDK 风格的项目。

关于单框架和多框架项目,就是项目文件中这里的差别:

1
2
3
4
5
6
7
8
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <!-- 单框架项目 -->
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

</Project>
1
2
3
4
5
6
7
8
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <!-- 多框架项目 -->
    <TargetFrameworks>netcoreapp3.1;net48</TargetFrameworks>
  </PropertyGroup>

</Project>

但是,有的小伙伴希望探索一些更高级的用法,所以可能会遇到在多框架项目中,NuGet 包自定义的功能不执行的问题。

接下来,我们了解一下在单框架和多框架下 NuGet 包执行上的不同。

执行时机

我们打出这样的两种 NuGet 包,一种是仅包含 build 文件夹而不包含 buildMultiTargeting 文件夹;一种是包含 build 文件夹和 buildMultiTargeting 文件夹。

我们的目标项目一种是单框架项目;一种是多框架项目。

于是我们可以得到这样的四种不同的组合情况:

  1. 仅含 build 文件夹的 NuGet 包装到单框架项目中
  2. 仅含 build 文件夹的 NuGet 包装到多框架项目中
  3. 包含 build 和 buildMultiTargeting 文件夹的 NuGet 包装到单框架项目中
  4. 包含 build 和 buildMultiTargeting 文件夹的 NuGet 包装到多框架项目中

1. 仅含 build 文件夹的 NuGet 包装到单框架项目中

在这种情况下,build 文件夹中的 .props 和 .targets 文件在目标项目编译时正常执行。

2. 仅含 build 文件夹的 NuGet 包装到多框架项目中

在这种情况下,build 文件夹中的 .props 和 .targets 文件,会分别在目标项目编译每个框架的时候执行一次。

例如这种项目:

1
2
3
4
5
6
7
8
9
10
11
12
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <!-- 多框架项目 -->
    <TargetFrameworks>netcoreapp3.1;net48</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Walterlv.NullableAttributes.Source" Version="0.15.0" />
  </ItemGroup>

</Project>

那么,在编译 netcoreapp3.1 框架的时候会执行一次 Walterlv.NullableAttributes.Source 包中 build 文件夹中的编译任务;在编译 net48 框架的时候又会执行一次 Walterlv.NullableAttributes.Source 包中 build 文件夹中的编译任务。

3. 包含 build 和 buildMultiTargeting 文件夹的 NuGet 包装到单框架项目中

在这种情况下,buildMultiTargeting 中的任何编译任务相当于不存在。编译过程与情况 1 是完全一样的。

4. 包含 build 和 buildMultiTargeting 文件夹的 NuGet 包装到多框架项目中

从 NuGet 5.x 版本开始在这种情况下,build 中的内容和 buildMultiTargeting 中的编译任务会同时参与编译。

依然举例这样的目标项目(不过使用了含 buildMultiTargeting 的 NuGet 包):

1
2
3
4
5
6
7
8
9
10
11
12
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <!-- 多框架项目 -->
    <TargetFrameworks>netcoreapp3.1;net48</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Walterlv.NullableAttributes.Source" Version="2.1.1" />
  </ItemGroup>

</Project>

编译一开始,会将 buildMultiTargeting 中的编译任务加入执行。在编译 netcoreapp3.1 框架的时候会执行一次 Walterlv.NullableAttributes.Source 包中 build 文件夹中的编译任务;在编译 net48 框架的时候又会执行一次 Walterlv.NullableAttributes.Source 包中 build 文件夹中的编译任务。而这两个单独框架的编译结束后,buildMultiTargeting 中的任务才会结束。

也就是说,这两个编译任务文件夹中的编译任务是都会执行的。但是:

两者参与编译的 Targets 不一样。

下表中列出了在你没有编写任何扩展的任务或者干预已有 Target 执行的情况下,默认可以依赖的 Target(指的是可以通过 BeforeTargets="xx" 或 AfterTargets="xx" 的方式扩展编译任务:

可依赖的 TargetbuildbuildMultiTargeting
BeforeCompile✔❌
Compile✔❌
CoreCompile✔❌
AfterCompile✔❌
BeforeBuild✔❌
Build✔✔
AfterBuild✔❌
BeforeRebuild❌❌
Rebuild❌✔(如果强行执行)
AfterRebuild❌❌
BeforeClean✔(如果强行执行)❌
Clean✔(如果强行执行)✔(如果强行执行)
AfterClean✔(如果强行执行)❌

注:强制执行说的是一般编译时不会执行,你需要在命令中指定执行这个 Target。也对应到 Visual Studio 里的“重新编译”和“清理”的功能。

为了更好理解上表,这里给出一个例子。下面的代码如果在 build 文件夹中则会在编译过程输出一堆星号,而如果在 buildMultiTargeting 文件夹中则不会执行。而无论目标项目是否是多框架的。但换成 AfterBuild 则会两个文件夹中都输出。

1
2
3
<Target Name="WalterlvDemoTarget" AfterTargets="Build">
    <Message Text="****************************************************************" />
</Target>

当然,不要被这个第 4 种情况带歪了!如果你的 NuGet 包依然只有一个 build 文件夹,那么上面的所有 Targets 都是会执行的。


参考资料

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/build-multi-targeting-nuget-package.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected]) 。

登录 GitHub 账号进行评论