Sunday, May 29, 2022

NuGet include native DLL

July Update : The advice in this post works for typical NuGet consumption, such as adding a package to a Visual Studio solution. However, it was found that #r "NuGet: XXX" in a csx script file does not cause the native DLLs in the package to be downloaded. At a guess, the custom build targets are not processed outside of the Visual Studio process.

For most people this script problem wouldn't be a concern, but unfortunately, our package was designed to be used with equal ease by csx scripting and Visual Studio projects. The only answer was to rewrite the C++ native DLL in C# so that the problem this post addresses simply went away. Removing the native DLL was like having a headache cured.

The original post remains here, just in case it might be useful to other developers.

I had to create a NuGet package which included a native DLL written in C++. Utility functions in the native DLL are called from C# using standard Interop techniques. The projects consuming the package may be either the traditional Framework format or the newer Sdk format. The vital requirement is that the native DLL is copied to the build output folder in the same way as managed DLLs.

The documentation on packaging and delivering extra files via NuGet is vague and confusing, and web searches produce hundreds of equally confusing and often contradictory arguments on the subject. After weeks of part-time research and suffering I finally stumbled upon the answer by merging hints from a variety of sources.

Note that a group like the following in the source package project will produce the desired behaviour when consumed by an Sdk project, but has no effect in a Framework project.

  <ItemGroup>
    <None Include="{Path to the native DLL}">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      <Pack>True</Pack>
      <PackagePath>contentFiles\any\any</PackagePath>
      <PackageCopyToOutput>true</PackageCopyToOutput>
    </None>
  </ItemGroup>

I will pretend that the source project name and namespace is Acme.Widget.

To get the desired behaviour in both types of consuming projects. Add a file named Acme.Widget.targets to the source project with action None and Do not copy.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <None Include="$(MSBuildThisFileDirectory)\native-utility.dll">
      <Link>native-utility.dll</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

This targets file will be inserted into the receiving project's build processing where it will extract the native DLL from the package and copy it to the build output folder.

Add the following items to the source project.

  <ItemGroup>
    <None Include="Acme.Widgets.targets">
      <CopyToOutputDirectory>Never</CopyToOutputDirectory>
      <Pack>True</Pack>
      <PackagePath>build\</PackagePath>
      <PackageCopyToOutput>true</PackageCopyToOutput>
    </None>
  </ItemGroup>

  <ItemGroup>
    <None Include="{path to native-utility.dll}" Link="native-utility.dll">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
      <Pack>True</Pack>
      <PackagePath>build\</PackagePath>
      <PackageCopyToOutput>true</PackageCopyToOutput>
    </None>
  </ItemGroup>

The first item places the targets file into the special build folder in the package. The NuGet system recognises that the package wants to modify the receiving project's build processing.

The second item links the native DLL into the source project so it can be used normally for development and testing. It also adds it to the build folder in the package.

In summary, the trick is to put a targets file and the native DLL into the build folder in the package. When a project receives the package, the targets file is inserted into the build process and it copies the native DLL to the build output folder. This creates the illusion that the native DLL behaves just like a normal managed DLL.

I remain suspicious that my instructions can be simplified or follow conventions that are currently unknown to me. The whole process seems too complicated. Expert feedback would be welcome.

No comments:

Post a Comment