Fügen Sie native Dateien aus dem NuGet-Paket zum Projektausgabeverzeichnis hinzu

126

Ich versuche, ein NuGet-Paket für eine .Net-Assembly zu erstellen, die Pinvoke für eine native Win32-DLL ausführt. Ich muss sowohl die Assembly als auch die native DLL packen, wobei die Assembly zu den Projektreferenzen hinzugefügt wird (in diesem Teil kein Problem), und die native DLL sollte in das Projektausgabeverzeichnis oder ein anderes relatives Verzeichnis kopiert werden.

Meine Fragen sind:

  1. Wie packe ich die native DLL, ohne dass Visual Studio versucht, sie in die Referenzliste aufzunehmen?
  2. Muss ich eine install.ps1 schreiben, um die native DLL zu kopieren? Wenn ja, wie kann ich auf den Paketinhalt zugreifen, um ihn zu kopieren?
AlonFStackoverflow
quelle
1
Es gibt Unterstützung für Laufzeit- / Architektur-spezifische Bibliotheken, aber die Funktionsdokumentation fehlt und es scheint UWP-spezifisch zu sein. docs.microsoft.com/en-us/nuget/create-packages/…
Wouter

Antworten:

131

Wenn Sie das CopyZiel in der Zieldatei zum Kopieren der erforderlichen Bibliotheken verwenden, werden diese Dateien nicht in andere Projekte kopiert, die auf das Projekt verweisen DllNotFoundException. Dies kann jedoch mit einer viel einfacheren Zieldatei unter Verwendung eines NoneElements erfolgen, da MSBuild alle NoneDateien in referenzierende Projekte kopiert .

<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>

Fügen Sie die Zieldatei buildzusammen mit den erforderlichen nativen Bibliotheken zum Verzeichnis des Nuget-Pakets hinzu. Die Zieldatei enthält alle dllDateien in allen untergeordneten Verzeichnissen des buildVerzeichnisses. Wenn Sie also eine x86und x64-Version einer nativen Bibliothek hinzufügen, die von einer Any CPUverwalteten Assembly verwendet wird, erhalten Sie eine Verzeichnisstruktur, die der folgenden ähnelt:

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

Das gleiche x86und x64Verzeichnisse werden beim Erstellen im Ausgabeverzeichnis des Projekts erstellt. Wenn Sie keine Unterverzeichnisse benötigen, können die **und %(RecursiveDir)entfernt werden und stattdessen die erforderlichen Dateien builddirekt in das Verzeichnis aufnehmen. Auf die gleiche Weise können auch andere erforderliche Inhaltsdateien hinzugefügt werden.

Die wie Nonein der Zieldatei hinzugefügten Dateien werden beim Öffnen in Visual Studio nicht im Projekt angezeigt. Wenn Sie sich fragen, warum ich den ContentOrdner im nupkg nicht verwende, liegt dies daran, dass es keine Möglichkeit gibt, das CopyToOutputDirectoryElement ohne Verwendung eines Powershell-Skripts festzulegen (das nur in Visual Studio ausgeführt wird, nicht über die Eingabeaufforderung, auf Build-Servern oder in andere IDEs und wird in project.json / xproj-DNX-Projekten nicht unterstützt. Ich bevorzuge die Verwendung von a Linkfür die Dateien, anstatt eine zusätzliche Kopie der Dateien im Projekt zu haben.

Update: Obwohl dies auch funktionieren sollte, Contentanstatt Nonedass es einen Fehler in msbuild gibt, werden Dateien nicht mehr als einen Schritt entfernt in Referenzprojekte kopiert (z. B. proj1 -> proj2 -> proj3, proj3 erhält die Dateien nicht aus dem NuGet-Paket von proj1, aber proj2 wird).

kjbartel
quelle
4
Sir, Sie sind ein Genie! Klappt wunderbar. Vielen Dank.
MoonStom
Sie fragen sich, warum die Bedingung '$(MSBuildThisFileDirectory)' != '' And HasTrailingSlash('$(MSBuildThisFileDirectory)')erforderlich ist? Ich dachte, dass das MSBuildThisFileDirectoryimmer eingestellt ist. Wann wäre das nicht der Fall?
km
@kkm Ehrlich. Ich denke nicht, dass es gebraucht wird. Ich kann mich nicht einmal erinnern, woher ich es ursprünglich habe.
Kjbartel
@kkm Ich habe ursprünglich das System.Data.SQLite-Nuget-Paket geändert und es scheint, dass ich das hinter mir gelassen habe, als ich den ganzen anderen Mist entfernt habe, den sie enthalten haben. Ursprüngliche Zieldatei .
Kjbartel
2
@ SuperJMN Da gibt es Platzhalter. Hast du das nicht bemerkt **\*.dll? Das kopiert alle .dllDateien in alle Verzeichnisse. Sie können leicht **\*.*einen ganzen Verzeichnisbaum kopieren.
Kjbartel
30

Ich hatte kürzlich das gleiche Problem, als ich versuchte, ein EmguCV NuGet-Paket zu erstellen, das sowohl verwaltete Assemblys als auch nicht verwaltete gemeinsam genutzte Liraries (die ebenfalls in einem x86Unterverzeichnis abgelegt werden mussten ) enthielt und nach jedem Build automatisch in das Build-Ausgabeverzeichnis kopiert werden musste .

Hier ist eine Lösung, die ich gefunden habe und die nur auf NuGet und MSBuild basiert:

  1. Platzieren Sie die verwalteten Assemblys im /libVerzeichnis des Pakets (offensichtlicher Teil) und die nicht verwalteten gemeinsam genutzten Bibliotheken und zugehörigen Dateien (z. B. .pdb-Pakete) im /buildUnterverzeichnis (wie in den NuGet-Dokumenten beschrieben ).

  2. Benennen Sie alle nicht verwalteten *.dllDateiendungen in etwas anderes um, um beispielsweise *.dl_zu verhindern, dass NuGet darüber stöhnt, dass angebliche Assemblys an einer falschen Stelle platziert wurden ( "Problem: Assembly außerhalb des lib-Ordners" ).

  3. Fügen Sie <PackageName>.targetsim /buildUnterverzeichnis eine benutzerdefinierte Datei mit den folgenden Inhalten hinzu (eine Beschreibung finden Sie unten):

    <?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>
    

Die obige .targetsDatei wird bei einer Installation des NuGet-Pakets in der Zielprojektdatei eingefügt und ist für das Kopieren der nativen Bibliotheken in das Ausgabeverzeichnis verantwortlich.

  • <AvailableItemName Include="NativeBinary" /> Fügt ein neues Element "Aktion erstellen" für das Projekt hinzu (das auch in der Dropdown-Liste "Aktion erstellen" in Visual Studio verfügbar ist).

  • <NativeBinary Include="...Fügt die nativen Bibliotheken hinzu, die /build/x86dem aktuellen Projekt hinzugefügt wurden, und macht sie für das benutzerdefinierte Ziel zugänglich, das diese Dateien in das Ausgabeverzeichnis kopiert.

  • <TargetPath>x86</TargetPath>Fügt den Dateien benutzerdefinierte Metadaten hinzu und weist das benutzerdefinierte Ziel an, die nativen Dateien in das x86Unterverzeichnis des tatsächlichen Ausgabeverzeichnisses zu kopieren .

  • Der <PrepareForRunDependsOn ...Block fügt das benutzerdefinierte Ziel der Liste der Ziele hinzu, von denen der Build abhängt. Weitere Informationen finden Sie in der Datei Microsoft.Common.targets .

  • Das benutzerdefinierte Ziel CopyNativeBinariesenthält zwei Kopieraufgaben. Der erste ist dafür verantwortlich, alle *.dl_Dateien in das Ausgabeverzeichnis zu kopieren und ihre Erweiterung wieder auf das Original zu ändern *.dll. Der zweite kopiert einfach den Rest (zum Beispiel alle *.pdbDateien) an denselben Speicherort. Dies könnte durch eine einzelne Kopieraufgabe und ein install.ps1- Skript ersetzt werden, in das alle *.dl_Dateien *.dllwährend der Paketinstallation umbenannt werden mussten.

Diese Lösung würde jedoch die nativen Binärdateien immer noch nicht in das Ausgabeverzeichnis eines anderen Projekts kopieren, das auf dasjenige verweist, das ursprünglich das NuGet-Paket enthält. Sie müssen das NuGet-Paket auch in Ihrem "endgültigen" Projekt referenzieren.

Buygrush
quelle
4
" Diese Lösung würde jedoch die nativen Binärdateien immer noch nicht in das Ausgabeverzeichnis eines anderen Projekts kopieren, das auf dasjenige verweist, das ursprünglich das NuGet-Paket enthält. Sie müssen das NuGet-Paket auch in Ihrem" endgültigen "Projekt referenzieren. " Dies ist ein zeige Stopper für mich. Dies bedeutet normalerweise, dass Sie das Nuget-Paket mehreren Projekten hinzufügen müssen (z. B. Unit-Tests), da Sie sonst DllNotFoundExceptiongeworfen werden.
Kjbartel
2
etwas drastisch, um die Dateien usw. nur wegen der Warnung umzubenennen.
Sie können die Warnung entfernen, indem Sie sie <NoWarn>NU5100</NoWarn>zu Ihrer Projektdatei hinzufügen
Florian Koch
28

Hier ist eine Alternative , die die verwendet , .targetsum die native DLL in dem Projekt zu injizieren mit den folgenden Eigenschaften.

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

Der Hauptvorteil dieser Technik besteht darin, dass die native DLL transitiv in den bin/Ordner abhängiger Projekte kopiert wird .

Siehe das Layout der .nuspecDatei:

Screenshot von NuGet Package Explorer

Hier ist die .targetsDatei:

<?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>

Dadurch wird das eingefügt, MyNativeLib.dllals ob es Teil des ursprünglichen Projekts wäre (aber seltsamerweise ist die Datei in Visual Studio nicht sichtbar).

Beachten Sie das <Link>Element, das den Namen der bin/Zieldatei im Ordner festlegt .

Benoit Blanchon
quelle
Funktioniert mit einigen .bat- und .ps1-Dateien, die ich als Teil meines Azure-Dienstes einbinden muss - danke :)
einbinden muss -
"(Aber seltsamerweise ist die Datei in Visual Studio nicht sichtbar)." - Projektdateien werden von VS selbst AFAIK analysiert, sodass Elemente, die in externen Zieldateien hinzugefügt wurden (oder solche, die dynamisch bei der Zielausführung erstellt wurden), nicht angezeigt werden.
km
Wie unterscheidet sich das von der anderen früheren Antwort als dem Wechsel von Contentzu None?
Kjbartel
3
Wow, du bist schnell. Wenn Sie sich dazu entschließen, könnten Sie zumindest fragen, wie sich das von meiner Antwort unterscheidet. Dieses Imo wäre fairer, als die ursprüngliche Frage zu bearbeiten, sie selbst zu beantworten und dann Ihre Antwort in den Kommentaren anderer zu bewerben.
Ganz
3
@MaksimSatsikau Vielleicht möchten Sie sich die Geschichte ansehen. Ich habe die Frage bearbeitet, um sie klarer zu machen, und dann die Frage beantwortet. Diese Antwort kam ein paar Wochen später und war praktisch eine Kopie. Entschuldigung, wenn ich das unhöflich fand.
Kjbartel
19

Wenn jemand anderes darüber stolpert.

Der .targetsDateiname MUSS der NuGet-Paket-ID entsprechen

Alles andere wird nicht funktionieren.

Credits gehen an: https://sushihangover.github.io/nuget-and-msbuild-targets/

Ich hätte gründlicher lesen sollen, wie es hier tatsächlich vermerkt ist. Hat ewig gedauert ..

Fügen Sie eine benutzerdefinierte hinzu <PackageName>.targets

DirtyLittleHelper
quelle
3
Du rettest meinen ganzen Tag!
Zheng Yu
1
Sie haben ein einwöchiges Problem mit etwas anderem behoben. Danke dir und dieser Github-Seite.
Glenn Watson
13

Es ist ein bisschen spät, aber ich habe genau dafür ein Nuget-Paket erstellt.

Die Idee ist, einen zusätzlichen speziellen Ordner in Ihrem Nuget-Paket zu haben. Ich bin sicher, Sie kennen Lib und Content bereits. Das von mir erstellte Nuget-Paket sucht nach einem Ordner mit dem Namen "Ausgabe" und kopiert alles, was sich dort befindet, in den Ausgabeordner des Projekts.

Sie müssen dem Paket nur eine Nuget-Abhängigkeit hinzufügen. Http://www.nuget.org/packages/Baseclass.Contrib.Nuget.Output/

Ich habe einen Blog-Beitrag darüber geschrieben: http://www.baseclass.ch/blog/Lists/Beitraege/Post.aspx?ID=6&mobile=0

Daniel Romero
quelle
Das ist großartig! Dies funktioniert jedoch nur im aktuellen Projekt. Wenn das Projekt eine "Klassenbibliothek" ist und Sie beispielsweise eine Abhängigkeit von einer "Webanwendung" hinzufügen möchten, werden die DLLs nicht in einer Webanwendung erstellt! Meine "schnelle Lösung" lautet: Erstellen Sie ein NuGet für Ihre Bibliothek und wenden Sie es auf die Klassenbibliothek an. Erstellen Sie ein weiteres Nuget für Abhängigkeiten (in diesem Fall DLLs) und wenden Sie es auf WebApplication an. Irgendeine beste Lösung dafür?
Wagner Leonardi
Sie haben dieses Projekt anscheinend nur für .NET 4.0 (Windows) erstellt. Planen Sie eine Aktualisierung, um auch tragbare Klassenbibliotheken zu unterstützen?
Ani
1

Es gibt eine reine C # -Lösung, die ich ziemlich einfach finde und mit der ich mich nicht mit NuGet-Einschränkungen beschäftigen muss. Folge diesen Schritten:

Fügen Sie die native Bibliothek in Ihr Projekt ein und setzen Sie die Build Action-Eigenschaft auf Embedded Resource.

Fügen Sie den folgenden Code in die Klasse ein, in der Sie diese Bibliothek aufrufen.

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());
    }
}

Rufen Sie diese Methode wie folgt vom statischen Konstruktor auf UnpackNativeLibrary("win32");und entpacken Sie die Bibliothek auf die Festplatte, bevor Sie sie benötigen. Natürlich müssen Sie sicherstellen, dass Sie über Schreibberechtigungen für diesen Teil der Festplatte verfügen.

Ondrej Janacek
quelle
1

Dies ist eine alte Frage, aber ich habe jetzt das gleiche Problem und habe einen Turnaround gefunden, der etwas schwierig, aber sehr einfach und effektiv ist: Erstellen Sie im Nuget-Standardinhaltsordner die folgende Struktur mit einem Unterordner für jede Konfiguration:

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

Wenn Sie die Nuspec-Datei packen, erhalten Sie für jede native Bibliothek in den Ordnern Debug und Release die folgende Meldung:

Problem: Assembly außerhalb des lib-Ordners. Beschreibung: Die Assembly 'Content \ Bin \ Debug \ ??????. DLL' befindet sich nicht im Ordner 'lib' und wird daher nicht als Referenz hinzugefügt, wenn das Paket in einem Projekt installiert wird. Lösung: Verschieben Sie es in den Ordner 'lib', wenn darauf verwiesen werden soll.

Wir brauchen keine solche "Lösung", weil dies nur unser Ziel ist: Native Bibliotheken werden nicht als NET Assemblies-Referenzen hinzugefügt.

Die Vorteile sind:

  1. Einfache Lösung ohne umständliche Skripte mit seltsamen Effekten, die bei der Deinstallation des Pakets nur schwer zurückgesetzt werden können.
  2. Nuget verwaltet die nativen Bibliotheken wie alle anderen Inhalte bei der Installation und Deinstallation.

Die Nachteile sind:

  1. Sie benötigen für jede Konfiguration einen Ordner (normalerweise gibt es jedoch nur zwei: Debug und Release. Wenn in jedem Konfigurationsordner andere Inhalte installiert werden müssen, ist dies entweder der richtige Weg).
  2. Native Bibliotheken müssen in jedem Konfigurationsordner dupliziert werden (wenn Sie jedoch für jede Konfiguration unterschiedliche Versionen der nativen Bibliotheken haben, ist dies entweder der richtige Weg).
  3. Die Warnungen für jede native DLL in jedem Ordner (aber wie gesagt, sie werden zur Paketzeit an den Paketersteller und nicht an den Paketbenutzer zur VS-Installationszeit ausgegeben).
SERWare
quelle
0

Ich kann Ihr genaues Problem nicht lösen, aber ich kann Ihnen einen Vorschlag machen.

Ihre Hauptanforderung lautet: "Und lassen Sie die Referenz nicht automatisch registrieren" .....

Sie müssen sich also mit "Lösungselementen" vertraut machen.

Siehe Referenz hier:

Hinzufügen von Elementen auf Lösungsebene zu einem NuGet-Paket

Sie müssen ein Powershell-Voodoo schreiben, um die Kopie Ihrer nativen DLL in ihre Heimat zu bringen (erneut, weil Sie nicht möchten, dass das Voodoo mit automatischer Referenzreferenz ausgelöst wird).

Hier ist eine ps1-Datei, die ich geschrieben habe ..... um Dateien in einem Referenzordner eines Drittanbieters abzulegen.

Es gibt genug für Sie, um herauszufinden, wie Sie Ihre native DLL in ein "Zuhause" kopieren können ... ohne von vorne anfangen zu müssen.

Wieder ist es kein direkter Treffer, aber es ist besser als nichts.

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" 
granadaCoder
quelle
-2

Setzen Sie es ist der Inhaltsordner

Der Befehl nuget pack [projfile].csprojerledigt dies automatisch für Sie, wenn Sie die Dateien als Inhalt markieren.

Bearbeiten Sie dann die Projektdatei wie hier beschrieben und fügen Sie das Element ItemGroup & NativeLibs & None hinzu

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

arbeitete für mich

Sharon Lachs
quelle