145

I'm trying to create NuGet package for a .Net assembly which does pinvoke to a native win32 dll. I need to pack both the assembly and the native dll with the assembly added to the project references (no problem at this part) and the native dll should be copied into the project output directory or some other relative directory.

My questions are:

  1. How do I pack the native dll without visual studio trying to add it into the references list?
  2. Do I have to write an install.ps1 for copying the native dll? If so how can I access the package content for copying it?
kjbartel
  • 10,381
  • 7
  • 45
  • 66
AlonFStackoverflow
  • 1,481
  • 2
  • 10
  • 5
  • 2
    There is support for runtime/architecture specific libraries, but the feature documentation is lacking and it seems to be UWP specific. https://learn.microsoft.com/en-us/nuget/create-packages/supporting-multiple-target-frameworks#architecture-specific-folders – Wouter Jan 21 '19 at 12:49

10 Answers10

153

Using the Copy target in the targets file to copy required libraries won't copy those files to other projects which reference the project, resulting in a DllNotFoundException. This can be done with a much simpler targets file though, using a None element, as MSBuild will copy all None files to referencing projects.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <NativeLibs Include="$(MSBuildThisFileDirectory)**\*.dll" />
    <None Include="@(NativeLibs)">
      <Link>%(RecursiveDir)%(FileName)%(Extension)</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

Add the targets file to the build directory of the nuget package along with the required native libraries. The targets file will include all dll files in all child directories of the build directory. So to add an x86 and x64 version of a native library used by an Any CPU managed assembly you would end up with a directory structure similar to the following:

  • build
    • x86
      • NativeLib.dll
      • NativeLibDependency.dll
    • x64
      • NativeLib.dll
      • NativeLibDependency.dll
    • MyNugetPackageID.targets
  • lib
    • net40
      • ManagedAssembly.dll

The same x86 and x64 directories will be created in the project's output directory when built. If you don't need subdirectories then the ** and the %(RecursiveDir) can be removed and instead include the required files in the build directory directly. Other required content files can also be added in the same way.

The files added as None in the targets file won't be shown in the project when open in Visual Studio. If you are wondering why I don't use the Content folder in the nupkg it's because there's no way to set the CopyToOutputDirectory element without using a powershell script (which will only be run inside Visual Studio, not from the command prompt, on build servers or in other IDEs, and is not supported in project.json / xproj DNX projects) and I prefer to use a Link to the files rather than having an additional copy of the files within the project.

Update: Although this should also work with Content rather than None it appears that there's a bug in msbuild so files won't be copied to referencing projects more than one step removed (e.g. proj1 -> proj2 -> proj3, proj3 won't get the files from proj1's NuGet package but proj2 will).

Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
kjbartel
  • 10,381
  • 7
  • 45
  • 66
  • 5
    Sir, you are a genius! Works like a charm. Thanks. – MoonStom Oct 02 '15 at 19:17
  • Wondering why is the condition `'$(MSBuildThisFileDirectory)' != '' And HasTrailingSlash('$(MSBuildThisFileDirectory)')` required? I thought that the `MSBuildThisFileDirectory` is always set. When that would not be the case? – kkm inactive - support strike Oct 23 '15 at 04:06
  • @kkm Honestly. I don't think it is needed. I can't even recall where I originally got it from. – kjbartel Oct 23 '15 at 05:29
  • @kkm I originally modified the System.Data.SQLite nuget package and it seems I left that behind when I removed all the other crap they included. [Original targets file](http://system.data.sqlite.org/index.html/artifact/79fe57c8f46e0e61). – kjbartel Oct 23 '15 at 05:39
  • Thanks much for the clarification! – kkm inactive - support strike Oct 23 '15 at 18:58
  • Nice, but... what if you want to copy an entire folder instead of a single file? This solution doesn't scale well if there are more than 2 files... :( – SuperJMN Dec 11 '15 at 17:43
  • 2
    @SuperJMN There's wildcards there. Didn't you notice the `**\*.dll`? That's copying all `.dll` files in all directories. You could easily do `**\*.*` to copy a whole directory tree. – kjbartel Dec 14 '15 at 00:02
  • Sorry! My fault. It is working like a charm. I didn't read it carefully. Thanks for your clarification! – SuperJMN Dec 14 '15 at 09:42
  • @kjbartel I'm actually trying to use this solution to copy some managed dlls to the project output directory without referencing them in the project. However, I noticed that if my project already references different versions of these managed dlls these /build dlls are getting referenced instead. Any ideas? My related question is [here](http://stackoverflow.com/questions/36652956/visual-studio-incorrectly-referencing-assembly-from-nuget-package-build-directo) – Eric Roller May 03 '16 at 13:22
  • @kjbartel could you detail how this would work for the case where dlls will be situated in `x86` and `x64` directories in the output. How does the executing assembly (e.g. exe or test project) now how to and where to load the native dlls from e.g. either `x86` or `x64` directory depending on arch? – nietras Jun 02 '16 at 06:46
  • @harrydev You could do a similar thing to how Math.Net Numerics does it: https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/Providers/NativeProviderLoader.cs – kjbartel Jun 02 '16 at 07:55
  • @kjbartel that approach requires an indirection and rather tedious work to then call the libs. I have heard of approaches that instead change the environment variable PATH to add the x86 or x64 lib directories to this during startup, but this is difficult in e.g. a test project with no "Main", was hoping you had a solution for this. – nietras Jun 02 '16 at 08:41
  • @kjbartel or just call SetDllDirectory which seems like the best solution. – nietras Jun 02 '16 at 09:04
  • @harrydev Yep that would work. But you still need to detect the architecture and process type either way. – kjbartel Jun 03 '16 at 02:53
  • @kjbartel yeah but for x86 and x64 that is easy. However, this trick is a bit less ideal for unit test projects where there is no single entry point i.e. Main. Any ideas how to solve this? – nietras Jun 03 '16 at 10:50
  • Hello, my problem with this solution is that all my DLLs are copied into the bin folder and not into \bin\Debug\net452\win7-x64 for example (I'm on a .NET Core project). I understand it's because in my .xproj file OutputPath is set to `.\bin\` . This is really fine for me if my DLLs are in my bin folder but how to make my program find them? Thanks – Jérôme MEVEL Jun 07 '16 at 10:18
  • It's assumed that your program can already load the correct native library. Nuget can't solve that for you. You have to either have the correct dll search path or manually specify the path to the dll when you load it. https://msdn.microsoft.com/en-us/library/ms682586.aspx – kjbartel Jun 09 '16 at 07:28
  • @kjbartel This solution worked perfectly, just what I was looking for. The binaries output neatly into the x86/x64 target directory. A bit late to this exchange, but the SettDllDirectory() works perfectly for others that may be wondering. – user1451407 Feb 01 '18 at 20:54
  • @kjbartel excellent work! just used this and it works great and was just what we needed! – Nerdtron Dec 11 '18 at 04:23
  • @kjbartel I notice that when using this approach, the .targets file also ends up in the output directory when the project using the package is built. Any thoughts on how to prevent that? – Nerdtron Dec 11 '18 at 14:07
  • @kjbartel I figured it out. In the .targets file, in the NativeLibs element, I added an Exclude property. The trick was to use the same format as the Include and then specify *.targets. More specifically, I had to comply with this note in the MS docs, "You must specify the path for both attributes. If you use an absolute path to specify file locations in the Include attribute, you must also use an absolute path in the Exclude attribute; if you use a relative path in the Include attribute, you must also use a relative path in the Exclude attribute.". – Nerdtron Dec 11 '18 at 15:13
  • When applying this trick outside of the nuget-pack context, make sure not to add this to a project file if the native files you need to add are not static but build artifacts. Visual Studio will get really confused if you do. Instead, put them in a target file and make sure this target gets used in the build process. – chtenb Jan 31 '20 at 15:14
  • 2
    Really? I can't believe that this is the best solution NuGet Community has for 5 years – jenkas May 03 '20 at 14:18
  • This does work... Until you need to make two such native Nugets and use them within same project. In my case it was constantly complaining about duplicate items in my project (but there was no duplicates). What fixed me the issue is removing that element. Hope this helps somebody. – Gregory Stein Oct 26 '20 at 14:27
  • @GregoryStein if you have multiple packages including the same native libs then you should make a nuget package for just the the native libs alone and have the other packages depend on it. – kjbartel Oct 08 '21 at 07:43
  • 2
    @kjbartel I actually found the issue - I have given same name for the resources in this line of .targets file: `` And this caused the conflict. Instead, you have to give different names in different nuget packages. For example: '`, '` – Gregory Stein Oct 10 '21 at 09:03
  • I tried this today with `None` and `Content`, but in both cases copying will only be done in directly dependend projects. Transitively dependend project do not get anything copied into their output directory. I am not using MSbuild, I am using dotnet CLI. – Kjara Feb 04 '22 at 09:00
  • @Kjara that's unfortunate to hear. I wonder if it's changed with SDK style projects and tooling. I'll have to retest. Thanks for the comment. – kjbartel Feb 07 '22 at 01:06
  • 1
    I found the solution already: It seems transitive dependency `.targets` are disabled per default. You need to opt-in by adjusting the value of `PrivateAssets`, see [here](https://github.com/NuGet/Home/issues/3342#issuecomment-459334177) or [ms docs](https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#controlling-dependency-assets). – Kjara Feb 07 '22 at 07:58
33

Here is an alternative that uses the .targets to inject the native DLL in the project with the following properties.

  • Build action = None
  • Copy to Output Directory = Copy if newer

The main benefit of this technique is that the native DLL is copied into the bin/ folder of dependent projects transitively.

See the layout of the .nuspec file:

Screen capture of NuGet Package Explorer

Here is the .targets file:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <ItemGroup>
        <None Include="$(MSBuildThisFileDirectory)\..\MyNativeLib.dll">
            <Link>MyNativeLib.dll</Link>
            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </None>
    </ItemGroup>
</Project>

This inserts the MyNativeLib.dll as if it was part of the original project (but curiously the file is not visible in Visual Studio).

Notice the <Link> element that sets the destination file name in the bin/ folder.

Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
Benoit Blanchon
  • 13,364
  • 4
  • 73
  • 81
  • Works a treat with some .bat and .ps1 files I need to include as part of my Azure service - thanks :) – Zhaph - Ben Duguid Jul 17 '15 at 13:19
  • “(but curiously the file is not visible in Visual Studio).” — project files are parsed by VS itself AFAIK, so items added in external .target files (or those dynamically created in target execution) are not shown. – kkm inactive - support strike Oct 23 '15 at 04:10
  • How is this any different to the [other earlier answer](http://stackoverflow.com/a/30316946/1730559) other than changing from `Content` to `None`? – kjbartel Feb 10 '16 at 09:46
  • @kjbartel 'other earlier answer', right. commenting to promote your own answers ftw – Maksim Satsikau Jun 24 '16 at 03:33
  • @MaksimSatsikau Commenting to note that it was an exact duplicate. Something which is frowned upon. – kjbartel Jun 24 '16 at 03:35
  • 3
    wow you're fast. anyway, if you chose to do so, you could at least ask 'how is this different from my answer'. that imo would be fairer than editing the original question, answering it yourself and then promoting your answer in other people's comments. not to mention that I personally like this particular answer better than yours - it's concise, to the point and easier to read – Maksim Satsikau Jun 24 '16 at 03:41
  • 4
    @MaksimSatsikau You might want to look at the history. I edited the question to make it clearer, then answered the question. This answer came a couple of weeks later and was effectively a copy. Sorry if I found that rude. – kjbartel Jun 29 '16 at 10:43
  • One crucial piece of information is that the `.targets` file must go in the `build` or `build/` folder in the package. So in your `.csproj` it would be included as follows: true build\netstandard2.1 Always true – stusmith Jan 06 '21 at 13:16
30

I had recently the same problem when I tried to build an EmguCV NuGet package including both managed assemblies and non-managed shared liraries (which also had to be placed in a x86 subdirectory) which had to be copied automatically to the build output directory after each build.

Here is a solution I came up with, that relies on only NuGet and MSBuild:

  1. Place the managed assemblies in the /lib directory of the package (obvious part) and the non-managed shared libraries and related files (e.g. .pdb packages) in the /build subdirectory (as described in the NuGet docs).

  2. Rename all non-managed *.dll file endings to something different, for example *.dl_ to prevent NuGet from moaning about alleged assemblies being placed at a wrong place ("Problem: Assembly outside lib folder.").

  3. Add a custom <PackageName>.targets file in the /build subdirectory with something like the following contents (see below for a description):

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <AvailableItemName Include="NativeBinary" />
  </ItemGroup>
  <ItemGroup>
    <NativeBinary Include="$(MSBuildThisFileDirectory)x86\*">
      <TargetPath>x86</TargetPath>
    </NativeBinary>
  </ItemGroup>
  <PropertyGroup>
    <PrepareForRunDependsOn>
      $(PrepareForRunDependsOn);
      CopyNativeBinaries
    </PrepareForRunDependsOn>
  </PropertyGroup>
  <Target Name="CopyNativeBinaries" DependsOnTargets="CopyFilesToOutputDirectory">
    <Copy SourceFiles="@(NativeBinary)"
          DestinationFiles="@(NativeBinary->'$(OutDir)\%(TargetPath)\%(Filename).dll')"
          Condition="'%(Extension)'=='.dl_'">
      <Output TaskParameter="DestinationFiles" ItemName="FileWrites" />
    </Copy>
    <Copy SourceFiles="@(NativeBinary)"
          DestinationFiles="@(NativeBinary->'$(OutDir)\%(TargetPath)\%(Filename).%(Extension)')"
          Condition="'%(Extension)'!='.dl_'">
      <Output TaskParameter="DestinationFiles" ItemName="FileWrites" />
    </Copy>
  </Target>
</Project>

The above .targets file will be injected on an installation of the NuGet package in the target project file and is responsible for copying the native libraries to the output directory.

  • <AvailableItemName Include="NativeBinary" /> adds a new item "Build Action" for the project (which also becomes available in the "Build Action" dropdown inside of Visual Studio).

  • <NativeBinary Include="... adds the native libraries placed in /build/x86 to current project and makes them accessible to the custom target which copies those files to the output directory.

  • <TargetPath>x86</TargetPath> adds custom metadata to the files and tells the custom target to copy the native files to the x86 subdirectory of the actual output directory.

  • The <PrepareForRunDependsOn ... block adds the custom target to the list of targets the build depends on, see the Microsoft.Common.targets file for details.

  • The custom target, CopyNativeBinaries, contains two copy tasks. The first one is responsible for copying any *.dl_ files to the output directory while changing their extension back to to the original *.dll. The second one simply copies the rest (for example any *.pdb files) to the same location. This could be replaced by a single copy task and an install.ps1 script which had to rename all *.dl_ files to *.dll during package installation.

However, this solution still would not copy the native binaries to the output directory of another project referencing the one which initially includes the NuGet package. You still have to reference the NuGet package in your "final" project as well.

Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
swalex
  • 3,885
  • 3
  • 28
  • 33
  • 4
    "_However, this solution still would not copy the native binaries to the output directory of another project referencing the one which initially includes the NuGet package. You still have to reference the NuGet package in your "final" project as well._" This is a show stopper for me. It usually means you need to add the nuget package to multiple projects (such as unit tests) otherwise you get `DllNotFoundException` thrown. – kjbartel May 22 '15 at 00:38
  • 2
    a bit drastic to rename the files etc just because of the warning. –  Oct 31 '16 at 23:27
  • you can remove the warning by adding `NU5100` to your project file – Florian Koch Nov 15 '19 at 10:29
24

If anyone else stumbles across this.

The .targets filename MUST equal the NuGet Package Id

Anything else wont work.

Credits go to: https://sushihangover.github.io/nuget-and-msbuild-targets/

I should've read more thoroughly as its actually noted here. Took me ages..

Add a custom <PackageName>.targets

13

It's a bit late but I've created a nuget package exaclty for that.

The idea is to have an additional special folder in your nuget package. I'm sure you already know Lib and Content. The nuget package I've created looks for a Folder named Output and will copy everything which is in there to the projects output folder.

The only thing you have to do is add a nuget dependency to the package http://www.nuget.org/packages/Baseclass.Contrib.Nuget.Output/

I've written a blog post about it: http://www.baseclass.ch/blog/Lists/Beitraege/Post.aspx?ID=6&mobile=0

Daniel Romero
  • 287
  • 2
  • 5
  • That's awesome! However, this works only in current project. If the project is a "Class Library" and you want to add as dependency to a "Web Application" for eg., the DLLs won't be builded in web application! My "quick-fix" is: create a NuGet for your library, and apply to Class Library, and create another Nuget for dependencies (dlls in this case) and apply to WebApplication. Any best solution for this? – Wagner Leonardi Mar 07 '14 at 22:41
  • You seem to have created this project for .NET 4.0 (Windows) only. Do you plan to update it to support portable class libraries as well? – Ani Jun 03 '15 at 04:58
3

There's a pure C# solution which I find rather easy to use and I don't have to bother with NuGet limitations. Follow these steps:

Include the native library in your project and set its Build Action property to Embedded Resource.

Paste the following code into class where you PInvoke this library.

private static void UnpackNativeLibrary(string libraryName)
{
    var assembly = Assembly.GetExecutingAssembly();
    string resourceName = $"{assembly.GetName().Name}.{libraryName}.dll";

    using (var stream = assembly.GetManifestResourceStream(resourceName))
    using (var memoryStream = new MemoryStream(stream.CanSeek ? (int)stream.Length : 0))
    {
        stream.CopyTo(memoryStream);
        File.WriteAllBytes($"{libraryName}.dll", memoryStream.ToArray());
    }
}

Call this method from the static constructor like as follows UnpackNativeLibrary("win32"); and it will unpack the library to disk just before you need it. Of course, you need to be sure that you have write permissions to that part of the disk.

Ondrej Janacek
  • 12,486
  • 14
  • 59
  • 93
  • This approach is way simpler and doesn't force users of your lib to pick x86 or x64 as it just works. I embedded both 32 and 64 bit native dlls and the unpack routine will create the necessary file based on runtime size of a ptr. I first test for existence and then test a native call and catch (BadImageFormatException e) and delete the file if wrong format. All this in about 12 lines of code and no complex nuget stuff and inherient support for AnyCpu. – Robert May 02 '21 at 15:08
  • @Robert Thanks for the comment. By now, having played with .NET SDK, I would use the nuget solution but this is still a nice fallback solution. – Ondrej Janacek May 03 '21 at 18:13
  • Curious? have things changed where user no longer has to explicitly set and build to x86 or x64 for nuget to handle both cases? – Robert May 04 '21 at 16:52
  • Yes. Check the answer from kjbartel. That's the solution I tend to use these days. – Ondrej Janacek May 05 '21 at 15:39
1

This is an old question, but I have the same problem now, and I found a turnaround that is a little tricky but very simple and effective: create in the Nuget standard Content folder the following structure with one subfolder for each configuration:

/Content
 /bin
   /Debug
      native libraries
   /Release
      native libraries

When you pack the nuspec file, you will receive the following message for each native library in the Debug and Release folders:

Issue: Assembly outside lib folder. Description: The assembly 'Content\Bin\Debug\??????.dll' is not inside the 'lib' folder and hence it won't be added as reference when the package is installed into a project. Solution: Move it into the 'lib' folder if it should be referenced.

We don't need such "solution" because this is just our goal: that native libraries aren't added as NET Assemblies references.

The advantages are:

  1. Simple solution with no cumbersome scripts with strange effects that are difficult to reset at package uninstall.
  2. Nuget manages the native libraries as any other content when installing and uninstalling.

The disadvantages are:

  1. You need a folder for each configuration (but usually there are only two: Debug and Release, and if you have other content that must be installed in each configuration folder, this is either the way to go)
  2. Native libraries must be duplicated in each configuration folder (but if you have different versions of the native libraries for each configuration, this is either the way to go)
  3. The warnings for each native dll in each folder (but as I said, they warnings are issued to the package creator at pack time, not to the package user at VS install time)
SERWare
  • 392
  • 3
  • 15
0

I can't solve your exact problem, but I can give you a suggestion.

Your key requirement is : "And have it not auto-register the reference".....

So you'll have to to become familiar with "solution items"

See reference here:

Adding solution-level items in a NuGet package

You'll have to write some powershell voodoo to get the copy of your native dll into its home (again, because you do NOT want the auto-add-reference voodoo to fire)

Here is a ps1 file I wrote.....to put files in a third party references folder.

There is enough there for you to figure out how to copy your native dll to some "home"...without having to start from scratch.

Again, its not a direct-hit, but its better than nothing.

param($installPath, $toolsPath, $package, $project)
if ($project -eq $null) {
$project = Get-Project
}

Write-Host "Start Init.ps1" 

<#
The unique identifier for the package. This is the package name that is shown when packages are listed using the Package Manager Console. These are also used when installing a package using the Install-Package command within the Package Manager Console. Package IDs may not contain any spaces or characters that are invalid in an URL.
#>
$separator = " "
$packageNameNoVersion = $package -split $separator | select -First 1

Write-Host "installPath:" "${installPath}"
Write-Host "toolsPath:" "${toolsPath}"
Write-Host "package:" "${package}"
<# Write-Host "project:" "${project}" #>
Write-Host "packageNameNoVersion:" "${packageNameNoVersion}"
Write-Host " "

<# Recursively look for a .sln file starting with the installPath #>
$parentFolder = (get-item $installPath)
do {
        $parentFolderFullName = $parentFolder.FullName

        $latest = Get-ChildItem -Path $parentFolderFullName -File -Filter *.sln | Select-Object -First 1
        if ($latest -ne $null) {
            $latestName = $latest.name
            Write-Host "${latestName}"
        }

        if ($latest -eq $null) {
            $parentFolder = $parentFolder.parent    
        }
}
while ($parentFolder -ne $null -and $latest -eq $null)
<# End recursive search for .sln file #>


if ( $parentFolder -ne $null -and $latest -ne $null )
{
    <# Create a base directory to store Solution-Level items #>
    $thirdPartyReferencesDirectory = $parentFolder.FullName + "\ThirdPartyReferences"

    if ((Test-Path -path $thirdPartyReferencesDirectory))
    {
        Write-Host "--This path already exists: $thirdPartyReferencesDirectory-------------------"
    }
    else
    {
        Write-Host "--Creating: $thirdPartyReferencesDirectory-------------------"
        New-Item -ItemType directory -Path $thirdPartyReferencesDirectory
    }

    <# Create a sub directory for only this package.  This allows a clean remove and recopy. #>
    $thirdPartyReferencesPackageDirectory = $thirdPartyReferencesDirectory + "\${packageNameNoVersion}"

    if ((Test-Path -path $thirdPartyReferencesPackageDirectory))
    {
        Write-Host "--Removing: $thirdPartyReferencesPackageDirectory-------------------"
        Remove-Item $thirdPartyReferencesPackageDirectory -Force -Recurse
    }

    if ((Test-Path -path $thirdPartyReferencesPackageDirectory))
    {
    }
    else
    {
        Write-Host "--Creating: $thirdPartyReferencesPackageDirectory-------------------"
        New-Item -ItemType directory -Path $thirdPartyReferencesPackageDirectory
    }

    Write-Host "--Copying all files for package : $packageNameNoVersion-------------------"
    Copy-Item $installPath\*.* $thirdPartyReferencesPackageDirectory -recurse
}
else
{
        Write-Host "A current or parent folder with a .sln file could not be located."
}


Write-Host "End Init.ps1" 
Community
  • 1
  • 1
granadaCoder
  • 26,328
  • 10
  • 113
  • 146
0

It is the exact purpose of the runtimes folder. More information about that on Microsoft Learn.

In short, you must place your architecture-specific native libraries into the right subfolder of runtimes:

runtimes/
  win10-x86/
    native/
      my-native.dll
  linux-x64/
    native/
      my-native.so

I successfully used this solution with pinvoke: depending on the runtime your program is running on, the DllImport will look for the right library from the right subfolder of runtimes.

-2

Put it is the content folder

the command nuget pack [projfile].csproj will do it for you automatically if you will mark the files as content.

then edit the project file as mentioned here adding ItemGroup & NativeLibs & None element

<ItemGroup>
    <NativeLibs Include="$(MSBuildThisFileDirectory)**\*.dll" />
    <None Include="@(NativeLibs)">
      <Link>%(RecursiveDir)%(FileName)%(Extension)</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
</ItemGroup>

worked for me