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:
How do I pack the native dll without visual studio trying to add it into the references list? 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?
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
x86 NativeLib.dll NativeLibDependency.dll
NativeLib.dll
NativeLibDependency.dll
x64 NativeLib.dll NativeLibDependency.dll
NativeLib.dll
NativeLibDependency.dll
MyNugetPackageID.targets
lib net40 ManagedAssembly.dll
net40 ManagedAssembly.dll
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).
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:
https://i.stack.imgur.com/nGB5G.png
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.
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:
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). 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."). Add a custom
<?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.
The
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.
DllNotFoundException
thrown.
<NoWarn>NU5100</NoWarn>
to your project file
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
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
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.
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:
Simple solution with no cumbersome scripts with strange effects that are difficult to reset at package uninstall. Nuget manages the native libraries as any other content when installing and uninstalling.
The disadvantages are:
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) 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) 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)
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"
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
Success story sharing
**\*.dll
? That's copying all.dll
files in all directories. You could easily do**\*.*
to copy a whole directory tree.<NativeLibs Include="$(MSBuildThisFileDirectory)**\*.dll" />
And this caused the conflict. Instead, you have to give different names in different nuget packages. For example: '<LibraryANativeLibs Include=...>, '<LibraryBNativeLibs Include=...>
.targets
are disabled per default. You need to opt-in by adjusting the value ofPrivateAssets
, see here or ms docs.