我正在尝试为 .Net 程序集创建 NuGet 包,该程序集确实可以调用本机 win32 dll。我需要将程序集和本机 dll 打包,并将程序集添加到项目引用中(这部分没有问题),并且应将本机 dll 复制到项目输出目录或其他一些相关目录中。
我的问题是:
如何在没有 Visual Studio 尝试将其添加到引用列表的情况下打包本机 dll?我是否必须编写一个 install.ps1 来复制本机 dll?如果是这样,我如何访问包内容以进行复制?
使用目标文件中的 Copy
目标复制所需的库不会将这些文件复制到引用该项目的其他项目,从而导致 DllNotFoundException
。这可以通过使用 None
元素的更简单的目标文件来完成,因为 MSBuild 会将所有 None
文件复制到引用项目。
<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>
将目标文件与所需的本机库一起添加到 nuget 包的 build
目录。目标文件将包括 build
目录的所有子目录中的所有 dll
文件。因此,要添加 Any CPU
托管程序集使用的本机库的 x86
和 x64
版本,您最终会得到类似于以下的目录结构:
构建 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
构建时,将在项目的输出目录中创建相同的 x86
和 x64
目录。如果您不需要子目录,则可以删除 **
和 %(RecursiveDir)
,而是直接在 build
目录中包含所需的文件。其他需要的内容文件也可以用同样的方法添加。
在 Visual Studio 中打开时,在目标文件中添加为 None
的文件不会显示在项目中。如果您想知道为什么我不在 nupkg 中使用 Content
文件夹,那是因为无法设置 CopyToOutputDirectory
元素 without using a powershell script(它只能在 Visual Studio 中运行,而不是在命令提示符下运行)构建服务器或其他 IDE,并且是 not supported in project.json / xproj DNX projects),我更喜欢对文件使用 Link
,而不是在项目中拥有文件的额外副本。
更新:虽然这也适用于 Content
而不是 None
,但似乎 msbuild 中存在一个错误,因此文件不会被复制到引用项目超过一个步骤删除(例如 proj1 - > proj2 -> proj3,proj3 不会从 proj1 的 NuGet 包中获取文件,但 proj2 会)。
这是一个替代方法,它使用 .targets
在项目中注入本机 DLL,并具有以下属性。
构建操作 = 无
复制到输出目录 = 如果更新则复制
这种技术的主要好处是本机 DLL 可传递地复制到 依赖项目 的 bin/
文件夹中。
查看 .nuspec
文件的布局:
https://i.stack.imgur.com/nGB5G.png
这是 .targets
文件:
<?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>
这会插入 MyNativeLib.dll
,就好像它是原始项目的一部分(但奇怪的是,该文件在 Visual Studio 中不可见)。
请注意在 bin/
文件夹中设置目标文件名的 <Link>
元素。
我最近在尝试构建包含托管程序集和非托管共享库(也必须放在 x86
子目录中)的 EmguCV NuGet 包时遇到了同样的问题每次构建后自动复制到构建输出目录。
这是我想出的一个解决方案,它仅依赖于 NuGet 和 MSBuild:
将托管程序集放在包的 /lib 目录中(显而易见的部分),将非托管共享库和相关文件(例如 .pdb 包)放在 /build 子目录中(如 NuGet 文档中所述)。将所有非托管 *.dll 文件结尾重命名为不同的名称,例如 *.dl_ 以防止 NuGet 抱怨所谓的程序集被放置在错误的位置(“问题:lib 文件夹外的程序集。”)。在 /build 子目录中添加一个自定义
<?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>
上面的 .targets
文件将在目标项目文件中安装 NuGet 包时注入,并负责将本机库复制到输出目录。
自定义目标 CopyNativeBinaries 包含两个复制任务。第一个负责将任何 *.dl_ 文件复制到输出目录,同时将它们的扩展名更改回原始 *.dll。第二个简单地将其余部分(例如任何 *.pdb 文件)复制到同一位置。这可以由单个复制任务和 install.ps1 脚本替换,该脚本必须在包安装期间将所有 *.dl_ 文件重命名为 *.dll。
但是,此解决方案仍然不会将本机二进制文件复制到另一个项目的输出目录,该目录引用最初包含 NuGet 包的项目。您仍然必须在“最终”项目中引用 NuGet 包。
DllNotFoundException
。
<NoWarn>NU5100</NoWarn>
添加到项目文件中来删除警告
如果其他人偶然发现这一点。
.targets
文件名必须等于 NuGet 包 ID
别的什么都行不通。
致谢:https://sushihangover.github.io/nuget-and-msbuild-targets/
我应该更彻底地阅读,因为它实际上在这里指出。花了我很多年..
添加自定义
这有点晚了,但我已经为此创建了一个 nuget 包。
这个想法是在您的 nuget 包中添加一个额外的特殊文件夹。我相信你已经知道 Lib 和 Content。我创建的 nuget 包查找名为 Output 的文件夹,并将其中的所有内容复制到项目输出文件夹中。
您唯一需要做的就是将 nuget 依赖项添加到包 http://www.nuget.org/packages/Baseclass.Contrib.Nuget.Output/
我写了一篇关于它的博客文章:http://www.baseclass.ch/blog/Lists/Beitraege/Post.aspx?ID=6&mobile=0
有一个纯 C# 解决方案,我觉得它很容易使用,而且我不必为 NuGet 的限制而烦恼。按着这些次序:
在您的项目中包含本机库并将其 Build Action 属性设置为 Embedded Resource
。
将以下代码粘贴到您 PInvoke 此库的类中。
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());
}
}
从静态构造函数调用此方法,如下所示UnpackNativeLibrary("win32");
,它将在您需要之前将库解包到磁盘。当然,您需要确保您对磁盘的该部分具有写入权限。
这是一个老问题,但我现在有同样的问题,我发现了一个有点棘手但非常简单有效的转变:在 Nuget 标准内容文件夹中创建以下结构,每个配置都有一个子文件夹:
/Content
/bin
/Debug
native libraries
/Release
native libraries
打包 nuspec 文件时,您将收到 Debug 和 Release 文件夹中每个本机库的以下消息:
问题:lib 文件夹外的程序集。说明:程序集“Content\Bin\Debug\??????.dll”不在“lib”文件夹中,因此在将包安装到项目中时不会将其添加为参考。解决方案:如果应该引用它,请将其移动到“lib”文件夹中。
我们不需要这样的“解决方案”,因为这只是我们的目标:不将本机库添加为 NET 程序集引用。
优点是:
简单的解决方案,没有繁琐的脚本,具有在软件包卸载时难以重置的奇怪效果。 Nuget 在安装和卸载时将本机库作为任何其他内容进行管理。
缺点是:
每个配置都需要一个文件夹(但通常只有两个:Debug 和 Release,如果每个配置文件夹中还有其他必须安装的内容,这也是可行的方法)每个配置中都必须复制原生库文件夹(但如果每个配置都有不同版本的本机库,这就是要走的路)每个文件夹中每个本机 dll 的警告(但正如我所说,它们在打包时向包创建者发出警告,而不是 VS 安装时的包用户)
我不能解决你的确切问题,但我可以给你一个建议。
您的关键要求是:“并且不要自动注册参考”.....
所以你必须熟悉“解决方案”
请参阅此处的参考:
Adding solution-level items in a NuGet package
您必须编写一些 powershell voodoo 才能将本机 dll 的副本复制到其主页中(同样,因为您不希望自动添加引用 voodoo 触发)
这是我写的一个 ps1 文件.....把文件放在第三方引用文件夹中。
那里有足够的东西让您弄清楚如何将您的本机 dll 复制到某个“家”……而不必从头开始。
同样,它不是直接命中,但总比没有好。
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"
放它是内容文件夹
如果您将文件标记为内容,命令 nuget pack [projfile].csproj
将自动为您执行此操作。
然后编辑这里提到的项目文件添加 ItemGroup & NativeLibs & None 元素
<ItemGroup>
<NativeLibs Include="$(MSBuildThisFileDirectory)**\*.dll" />
<None Include="@(NativeLibs)">
<Link>%(RecursiveDir)%(FileName)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
为我工作
**\*.dll
吗?这是复制所有目录中的所有.dll
文件。您可以轻松地执行**\*.*
来复制整个目录树。<NativeLibs Include="$(MSBuildThisFileDirectory)**\*.dll" />
这导致了冲突。相反,您必须在不同的 nuget 包中提供不同的名称。例如:'<LibraryANativeLibs Include=...>, '<LibraryBNativeLibs Include=...>
.targets
似乎被禁用。您需要通过调整PrivateAssets
的值来选择加入,请参阅 here 或 ms docs。