.NET Native Library Packaging (RuntimeIdentifiers, build, testing, VS etc.)
See original GitHub issuecc: @tannergooding @richlander @jkotas
This is yet another issue regarding how to best author native library nuget packages and define, build, test, publish deploy applications that consume these. I have tried hard to wrap my head about this by reading many issues and studying existing packages. I have a particular need that is similar to TorchSharp
with massive native libraries that not only need to be split into fragments but also where if possible it would be best only to “download” the runtime identifier (RID) specific packages needed for local development. (But on windows that local development often means BOTH x86 and x64 in our case).
Below I wrote a walk-through I did of using ClangSharp
(in excessive detail for reference) and the many questions that it raised for me compared to how I am used to working with this (based on our own way of authoring native library packages that are explicitly copied to sub-directories (x64
, x86
) alongside exe
and with those directories then added at runtime based on the process arch/os/system to dll directories i.e. via AddDllDirectory
. Having something “custom” is a maintenance issue of course, but also an on-boarding issue. Using documented best practices would be best, but as far as I can tell there are none?
In any case, at the end of the walk-through I encounter the problem that when specifying multiple RIDs i.e.
<RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers>
then the runtime.json
trick does not appear to work when running unit tests from inside Visual Studio. I have to explicitly add the RID specific nuget packages anyway, so I then wonder how exactly is one supposed to author nuget packages to be able to support running multiple RIDs (in this case solely interested in win-x86 and win-x64 for now) with full support for it as usual in VS and other tools? We need to be able to debug and run from VS?
And how do you switch which RID you run with when F5 running in VS?
Should I simply accept that the runtime.json
way is too flawed and explicitly reference all needed nuget packages? Would this then avoid the need to specify RIDs? Which also has issues with “forcing” self-contained (we don’t want that), in fact we’d like to simply be able to deploy/copy-paste build output as something like:
App.exe
win-x64\
// win-x64 specific native libraries
win-x86\
// win-x86 specific native libraries
where the app is not RID specific (framework-dependent of course). And this should work on both win-x86/winx64. This is what we have now and what works. Our developers are used to this. But it’s based on native library nuget packages that explicitly copy their native library contents to those folders and of course referencing all those RID specific ones. I had hoped perhaps one could avoid the RID specific referencing, but that does not seem to work “smoothly”. Which I’d guess then means the whole runtime.json
is not the way to go.
Secondly, I think I read somewhere (can’t find or remember where) that for .NET 8 it is considered to force a specific RID on build? I can see given my experience below why one might consider doing that, but that would then raise other issues such as losing what used to be a core tenant (IMHO) of .NET which is that a build output (not publish) is RID agnostic. Would that be lost then?
All in all, to solve these issues I have to author my own little tool for packaging the native libraries, consider all the issues around consumption, testing etc. And after going through all this I am still left with feeling rather lost 😅 I still don’t know exactly what is the best solution here. And the packages I am creating are intended to be published for the public, e.g. so I can publish the revived CNTK packages I’ve made on nuget.org for example.
On top of this we still want to support publishing RID specific applications, but then we don’t want native libraries embedded in single file, there is an option for that which is great, but then we want those dlls in sub-folder, not directly next to the exe
, which means we have to hack around that in MSBuild and then face issues with mixed-mode assemblies etc. Yes, we also have those which also makes things very interesting.
ML/AI isn’t going away. For each new CUDA or whatever release the native libraries double in size (minimum!). Easy authoring and consumption of those would be great, but I am sure also won’t be solved in the immediate future, I need to know what to do now?
The walk-through will come as the next comment.
Links
- “[Feature] Increase the package size limit on NuGet.org from 250 MB” https://github.com/NuGet/NuGetGallery/issues/9473 This features a discussion on how to split a nuget package and links.
- TorchSharp - package uses primary/fragment trick https://www.nuget.org/packages/libtorch-cuda-11.7-win-x64/
- SciSharp.TensorFlow.Redist - adopts same trick https://www.nuget.org/packages/SciSharp.TensorFlow.Redist-Linux-GPU#dependencies-body-tab
- “NuGet 3: The Runtime ID Graph” - discusses
runtime.json
https://natemcmaster.com/blog/2016/05/19/nuget3-rid-graph/ - “Improve handling of native packages (Support RID specific dependencies)” - shows how libclang uses this trick https://github.com/NuGet/Home/issues/10571 https://www.nuget.org/packages/libclang https://www.nuget.org/packages/libclang.runtime.win-x64/
- “MSBuild inline tasks with RoslynCodeTaskFactory” - need to join file fragments https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-roslyncodetaskfactory?view=vs-2022
- “Shipping a cross-platform MSBuild task in a NuGet package” https://natemcmaster.com/blog/2017/07/05/msbuild-task-in-nuget/
- “Architecture-specific folders like runtimes/<rid>/native/ outside of NuGet packages [nativeinterop]” https://github.com/dotnet/sdk/issues/24708
- “Should runtime. packages be listed in NuGet.org?” https://github.com/dotnet/core/issues/7568
- “Create a nuget package” https://github.com/vincenzoml/SimpleITK-dotnet-quickstart/issues/1
- “Add a way to list native assets that a project will load from the app directory (list them in deps.json)” https://github.com/dotnet/sdk/issues/11373
- “Guide for packaging C# library using P/Invoke to per-architecture and/or per-platform C++ native DLLs” https://github.com/NuGet/Home/issues/8623
- Mizux
dotnet-native
template git repository https://github.com/Mizux/dotnet-native
Issue Analytics
- State:
- Created 2 months ago
- Reactions:1
- Comments:17 (8 by maintainers)
Top GitHub Comments
This entire space has a large number of issues and there isn’t any good or “official” way to do things. Even
runtime.json
is itself a largely undocumented feature.ClangSharp is doing it the way it is primarily because of NuGet package size limits, but also because no one wants to download a single 256MB or larger package when they only need a 32MB subset of it.
Multiple issues, many of which you linked to in the OP, exist that track the general problem space.
ClangSharp
/libclang
WalkthroughCreate simple console application in for example a
Tester
directory.Add package reference to
ClangSharp
package socsproj
looks like:Run
dotnet restore -verbosity:detailed > restore.txt
on project. Verbosity set to be able to check what happens. Nothing of worth here. Look in.nuget
package cache to see what is downloaded:What’s interesting here is no RID specific packages appear to be downloaded (yet). The
ClangSharp
package has anuspec
file with:Jumping over the interop package and looking at
libClang
this nuspec has:That’s interesting given it has no dependencies and contains no libraries:
But what’s in the
runtime.json
file:Ah, that appears to map RIDs to runtime specific packages. But none were downloaded, so what happens when we build the project. Run
dotnet build -verbosity:detailed > build.txt
on project. Examining the build output and the.nuget
cache none of those runtime specific packages appear to be downloaded (yet). Let’s try running the project with some dummy code inProgram.cs
.It runs, but still no runtime specific packages downloaded nor any native libraries in build output. Let’s try a more involved example copied from a unit test in
ClangSharp
.This runs fine. But still no runtime specific packages downloaded nor any native libraries in build output. Let’s trying running the code in Visual Studio with native debugging enabled. That is add launch settings with
"nativeDebugging": true
. This is just a quick way to look at which native libraries are loaded and from where. Many ways of doing that, just using Visual Studio since quick and easy. In the Debug window one can see:Ah, turns out I have LLVM with clang installed 🤷 So this must be in environment variable
PATH
. Which it turns out it isC:\Program Files\LLVM\bin
. Let’s try removing that, and restart all consoles, applications in use.Running the example program again will then fail with exception:
Hmm, so the
libclang
native library is not available and the package is not downloaded automatically? How doesruntime.json
then work? Let’s try running the application with a runtime identifier defined:This takes a while, and only output is:
but the program runs fine. Looking in
.nuget
and we can see the runtime specific packages have actually been downloaded now.so what this means is we cannot actually run and define the application without specifying a runtime identifier? That’s seems problematic if we want to use this as framework dependent AnyCPU application… in fact if we run the application from Visual Studio again it will fail with the same exception as before.
Use
tree /F
to see the files in thebin
output, which shows all the native libraries related tolibclang
forwin-x64
(and others).Note how this has an
exe
under the specific runtime folder and all the dlls next to it.As far as I can tell this means the
runtime.json
way of mapping runtime identifier specific packages only works if you define a hard-coded specific runtime identifier in the program you want to run. Which is incredibly annoying if you want to build and deploy runtime agnostic applications. E.g. if we wanted to deploy awin-x86
+win-x64
single exe. How is that supposed to work then? Am I getting this wrong?Let’s try a hack. Adding the RID specific package to the project. That is add
<PackageReference Include="libclang.runtime.win-x64" Version="16.0.0" />
to the project. Run it from VS and then it now runs fine. Right, so in some ways this works fine if we add the RID specific packages explicitly.Still how does this work with regards to testing and if you use MSTest for both x86 and x64 testing? Let’s add a unit test project and reference the tester console project, and copy code from above unit test in
Program.cs
into this project. Now if we run the unit test with Processor Architecture for AnyCPU Projects set toAuto
. If we change this tox86
and it will fail with the same exception as before:Interestingly, in the output we will get:
Note that the RID is
win10-x86
in this case if logged e.g. withlog(RuntimeInformation.RuntimeIdentifier);
. If we selectx64
it iswin10-x64
and the test succeeds, but only because we added the RID specificlibclang.runtime.win-x64
package to the project.In https://github.com/dotnet/ClangSharp/issues/118#issuecomment-598305888 this issue is expanded upon with the comment by Tanner Gooding:
I wonder whether this actually works for the case of switching processor architecture in VS or similar? Let’s try adding it to the unit tests project and remove the RID specific package from the console project. Hence we have console project:
and unit test project:
First time you then try to build this you will get a well-known error:
So restore and build again. Let’s try running
x86
unit tests in VS. This succeeds but the RID is actually nowwin10-x64
, so we can now no longer run or debugx86
tests from Visual Studio?Let’s first try to define test running via a script
test-x86-x64.ps1
:For
x86
this will then fail with:again this isn’t great. We need to be able to run both x64 and x86 without having to go through hoops.
Perhaps if we add both
win-x64
andwin-x86
to aRuntimeIdentifiers
property instead? So changethen run
test-x86-x64.ps1
. Now everything fails with the same exception:According to https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#runtimeidentifiers I should have defined the RIDs correctly. An example from there is:
Okay, perhaps running tests then need to be done differently and not with the
RunConfiguration.TargetPlatform
property? Let’s try to run the tests with--runtime
instead in a new scripttest-x86-x64-rid.ps1
:Then the tests succeed, albeit with the annoying warnings below.
why do I need to specify whether to be self-contained or not when I am just running tests? I am not publishing?
And are the tests really running x86 as expected? To test this I add two simple test:
and run the tests again. On
win-x86
theX64
test fails as expected:and vice versa on
win-x64
:so at least that works as expected.
Let’s try running these tests from Visual Studio again. First, by setting processor architecture to
x86
. All tests exceptx86
fail, so this does switch the runtime identifier towin10-x86
, but it does not fix thelibclang
problem.so even though RIDs are now specified this doesn’t work when running tests from VS? Switching to
x64
in VS and then onlyX64
test passes, and still thelibclang
dll cannot be found, so now this doesn’t work either. The difference apparently being there is now multiple RIDs, not just one.Only way I think this can then be resolved is to actually explicitly add those RID specific runtime packages after all then so console project looks like:
Re-running the unit tests and now
libclang
can be loaded and that test succeeds. Let’s try command line too and it’s the same.So after all this, it seems like the
runtime.json
way of packaging native libraries has it’s set of challenges, you basically end up having explicitly add the RID specific packages anyway if you target multiple RIDs. In the process you then end up implicitly forcing the Any CPU build to no longer be frame dependent but self-contained? This is all very confusing and hard to understand and not the least convey to other developers.