帮助官方 NuGet 解掉 Bug,制作绝对不会传递依赖的 NuGet 包

如果你希望做一个 NuGet 工具包,那么这个包一定不能作为依赖传递给下一个包。典型的例子,做一个生成版本号的工具 NuGet 包,或者做一个代码分析器。

本文将解决 NuGet 的几个坑,真正做到绝对没有的依赖传递。


我们遇到了什么问题

如果你使用了 GitVersion 这款 NuGet 包来自动修改你的版本号,那么你可能会遇到这个问题。GitTools/GitVersion: Easy Semantic Versioning (http://semver.org) for projects using Git

假想我们希望开发一个 NuGet 包 Walterlv.PackageDemo.A。另一位小伙伴想要使用我 A 包的功能做一个 Walterlv.PackageDemo.B 包。于是其他小伙伴可以安装 B 包去做自己的项目 C。

那么,除非我在 B 包安装完之后,明确在 B 的 csproj 文件中写以下代码,否则 B 包发布出去后,安装 B 包的项目 C 就会同时安装上 A 包。

<ItemGroup>
  <PackageReference Include="Walterlv.PackageDemo.A" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>

显然,由于 A 是个工具包,只是为了给安装了 A 的 B 包提供版本号或其他编译期功能的。C 不需要这样的功能!

然而我们希望做出来的 A 包具备这样的特点:

  1. 小伙伴给 B 安装 A 包的时候,不用额外为 A 包写配置依赖的代码;
  2. 小伙伴为 C 安装 B 的时候,不会出现 A 乱入的情况。

如果你依然对这样的问题存有疑惑,可以阅读以下文章,这是切实的例子。

官方提供的解决方案

官方在非常早期的 2.7 版本就提供了 developmentDependency 属性,可以在 nuspec 文件中写。但实际上这个属性在后面版本的 NuGet 开发中就丢掉了。不生效。

官方提供了 IsTool 属性可以使用,但这依然不能阻止 B 安装了 A 包之后,C 包被迫安装 A 包的问题。

我试图寻找的解决方案

为 A 项目添加去除依赖的代码

我们创建一个项目 Walterlv.PackageDemo.A 模拟前面提到的包 A,创建一个项目 Walterlv.PackageDemo.B 模拟前面提到的包 B,创建一个项目 Walterlv.ProjectDemo.C 模拟前面的项目 C。注意,实际场景中,这三个项目通常在不同的仓库中,由不同的开发者开发。

创建项目 A、B、C

不过,为了方便起见,我打算直接在一个解决方案中模拟这样的效果:

在一个解决方案中模拟

我在 A 中试图创建一个 build\Walterlv.PackageDemo.A.props 或 build\Walterlv.PackageDemo.A.targets 文件,并在里面写一些阻止 A 被依赖的代码。

<Project>

  <ItemGroup>
    <PackageReference Update="Walterlv.PackageDemo.A" PrivateAssets="All" />
  </ItemGroup>

</Project>

为了通用一点,我取名为 Package.targets 文件,并在 A 项目编译的时候改名为 Walterlv.PackageDemo.A.targets。

Package.targets 文件
▲ 项目的结构

以下是 A 项目的 csproj 文件,包含将 Package.targets 在打包 NuGet 包时改名的部分。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

  <ItemGroup>
    <None Include="Assets\build\Package.targets" Pack="True" PackagePath="build\$(PackageId).targets" />
  </ItemGroup>

</Project>

在 B 项目中进行测试

本地调试当然用不着推送到 https://nuget.org。我们本地新建一个源,专门用于调试。

在 “工具 -> 选项 -> NuGet 包管理器” 中,我们可以设置 NuGet 源:

添加调试用的 NuGet 源
▲ 添加调试用的 NuGet 源

我们把刚刚 A 项目的输出目录填进去添加一个新的源。于是我们就能在 B 项目中安装 A 包了。

于是 B 项目的 csproj 文件全文内容如下:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Walterlv.PackageDemo.A" Version="1.0.0" />
  </ItemGroup>

</Project>

当以上 A 和 B 项目被 Visual Studio 编译的时候,一切符合预期;就像下图这样,B 项目中没有声明对 A 的依赖:

B 项目的 NuGet 包

令人遗憾的结果

然而使用命令行编译的时候,就不按照预期工作了;如下图这样,B 项目中出现了对 A 的依赖。

B 项目的 NuGet 包,有依赖

命令行编译时使用这些命令效果都是一样的不管用。

nuget restore
msbuild
dotnet restore
dotnet build

不过,令人难以置信的时,如果此时 Visual Studio 打开了此项目,命令行编译却能符合预期。

另外,我还尝试将 Package.targets 中的所有内容放到 <Target /> 里面以获得延迟到编译期执行的效果,但结论依然与上面一致,即仅能在 Visual Studio 中正常工作。

<Project>

  <Target Name="ForceWalterlvDemoPrivateAssets" BeforeTargets="CollectPackageReferences">
    <ItemGroup>
      <PackageReference Update="Walterlv.PackageDemo.A" PrivateAssets="All" />
    </ItemGroup>
  </Target>

</Project>

一个真的能解决依赖问题的方案

临时:在以上使用过程中额外发现命令行中存在不符合预期的结果,所以,这一节暂时注释。

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

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