NuGet 依赖管理的一个陷阱

就在刚才,在重新组织项目结构的时候,见识到了一个 NuGet 依赖管理造成的bug。这个bug很隐蔽,症状让人莫明其妙,初见成功让我乱了阵脚。


直到 DereTore v0.7.1 alpha 的时候,所有的项目目录都是直接位于解决方案根目录下的。这个解决方案一共有23个项目,在首页长长一串是很吓人的,所以我就将项目分类,放到各类别的子目录下了。调整完后,重新导入项目。此时出现了奇怪的事情,有些项目的引用出现了黄色的三角叹号,这表明引用解析失败。但是,那些是 .NET Framework 的程序集,比如 SystemSystem.Windows.FormsWindowsBase。于是我将这些引用删除,重新添加引用。然而,添加完成后,这些引用依然处于解析失败的状态。错误报告表示,这些程序集在 GAC 中找不到。但是,只要本机的 .NET Framework 不出问题,应该不会出现在 GAC 中找不到 .NET Framework 自身程序集的问题。.NET Framework Repair Tool 的检测表明本机的安装并没有问题。我只好先将没有引用错误的项目编译测试了。

解决方案中存在三种项目:

  1. 基础类库,只引用 .NET Framework 自身的程序集;
  2. 基础类库,引用了一些 NuGet 依赖;
  3. 其他,多是引用前二者的。

此时1是没问题的,2和3仍然不行。为了编译3,先要解决2。于是我在几个第二类项目中,删除 NuGet 依赖引用并重新通过 NuGet 包管理器加入。然而,重新加入的引用也是解析失败的。我快崩溃了,这个问题完全没用头绪,怎么解决呢?

首先排除代码错误导致这个问题的可能性。所有文件在迁移过程中都没发生变化,而代码的引用正确性是依赖于项目配置的,所以在代码发生错误之前,项目配置肯定出问题了。项目中的配置文件有哪些呢?

  1. app.config,指定了应用程序所用的一些配置值,和引用映射;
  2. packages.config,指定了所引用 NuGet 依赖的名称和版本;
  3. 容易被忘记的 *.csproj*.vbproj*.fsproj)和 .sln,这里面东西就多了。

查看一下 app.configpackages.config,它导致问题的可能性几乎为零,毕竟里面并没有存储路径,最多不过是程序集 ID 和版本。继续看 .csproj。看着看着,突然就发现了一个疑点:

1
2
3
4
5
6
7
<Import Project="..\packages\System.Data.SQLite.Core.1.0.104.0\build\net40\System.Data.SQLite.Core.targets" Condition="Exists('..\packages\System.Data.SQLite.Core.1.0.104.0\build\net40\System.Data.SQLite.Core.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\System.Data.SQLite.Core.1.0.104.0\build\net40\System.Data.SQLite.Core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\System.Data.SQLite.Core.1.0.104.0\build\net40\System.Data.SQLite.Core.targets'))" />
</Target>

所有的 NuGet 包都保存在解决方案根目录下的 packages 目录下。因此,这里正确的路径并不是 ..\packages\,而是 ..\..\packages\。我明明使用 NuGet 包管理器卸载再添加了依赖,这里却没发生变化——难道是因为这个导致了全面崩盘?删除所有外部程序集引用后,保存项目,删除上面所示的节点,再让 Visual Studio 重新加载项目,添加引用。嗯,这次就没问题了。将所有出现了无法解析引用的项目都如此处理后,调整代码,重新生成解决方案,生成成功。

这还没完,还有坑在。本地全部重新生成成功后,我提交并开了 PR。然而,CI 却报告生成失败。(幸好之前就熬过阵痛应用了 CI,真是现代生产力必备啊。)错误信息提示说有一个 NuGet 包提供的引用并没有自动还原。明明本地生成成功了,怎么回事?打开对应的 .csproj 后发现,其中的文件路径同样是 ..\packages\,但是我手工删除了 <Target> 节点。这样,Visual Studio 并没有因为引用缺失而报错。这个不报错,对 .NET Framework 的程序集引用也没报错。然而实际上对应的引用是找不到的,运行时可能会出错(我没试过)。于是我将相关的引用在文件中全部删除,重新回到 Visual Studio 再引用一遍。这次是真的没问题了。

结论:如果要调整带 NuGet 引用的项目的位置,保险起见,先删除 NuGet 依赖后,调整位置,再重新添加依赖。因为 NuGet 引用的依赖路径是相对路径,而且不会自动更新。


进一步发问:为什么项目引用(解决方案内,一个项目引用另一个项目的输出)不会发生这种麻烦的事情呢?我们来看看项目引用的例子:

1
2
3
4
<ProjectReference Include="..\..\Common\DereTore.Common.StarlightStage\DereTore.Common.StarlightStage.csproj">
<Project>{abd7f1b5-c4e3-4200-8e44-98b183dfee2c}</Project>
<Name>DereTore.Common.StarlightStage</Name>
</ProjectReference>

哎?这不也是相对路径吗?虽然是相对路径,也会出现黄色三角叹号,但是如果你使用 Visual Studio 删除引用,再在“添加”-“引用”-“项目”中添加项目引用,这一项的路径会随之更新。坑爹啊!为什么同样的操作就不会应用到 NuGet 依赖上呢?

分享到 评论