Ajouter des fichiers natifs du package NuGet au répertoire de sortie du projet

126

J'essaie de créer un package NuGet pour un assemblage .Net qui se connecte à une DLL win32 native. Je dois emballer à la fois l'assemblage et la dll native avec l'assemblage ajouté aux références du projet (pas de problème à cette partie) et la dll native doit être copiée dans le répertoire de sortie du projet ou dans un autre répertoire relatif.

Mes questions sont:

  1. Comment emballer la dll native sans que Visual Studio n'essaie de l'ajouter à la liste de références?
  2. Dois-je écrire un install.ps1 pour copier la DLL native? Si oui, comment puis-je accéder au contenu du package pour le copier?
AlonFStackoverflow
la source
1
Il existe un support pour les bibliothèques spécifiques à l'exécution / à l'architecture, mais la documentation sur les fonctionnalités fait défaut et semble être spécifique à UWP. docs.microsoft.com/en-us/nuget/create-packages/…
Wouter

Réponses:

131

L'utilisation de la Copycible dans le fichier de cibles pour copier les bibliothèques requises ne copiera pas ces fichiers dans d'autres projets qui référencent le projet, ce qui entraînera un fichier DllNotFoundException. Cela peut être fait avec un fichier de cibles beaucoup plus simple, en utilisant un Noneélément, car MSBuild copiera tous les Nonefichiers pour référencer les projets.

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

Ajoutez le fichier de cibles au buildrépertoire du package nuget avec les bibliothèques natives requises. Le fichier cibles comprendra tous les dllfichiers de tous les répertoires enfants du buildrépertoire. Donc, pour ajouter une version x86et x64d'une bibliothèque native utilisée par un Any CPUassembly géré, vous vous retrouveriez avec une structure de répertoire similaire à la suivante:

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

Les mêmes répertoires x86et x64seront créés dans le répertoire de sortie du projet lors de la construction. Si vous n'avez pas besoin de sous-répertoires, les **et les %(RecursiveDir)peuvent être supprimés et inclure à la place les fichiers requis builddirectement dans le répertoire. D'autres fichiers de contenu requis peuvent également être ajoutés de la même manière.

Les fichiers ajoutés comme Nonedans le fichier cibles ne seront pas affichés dans le projet lorsqu'ils sont ouverts dans Visual Studio. Si vous vous demandez pourquoi je n'utilise pas le Contentdossier dans le nupkg, c'est parce qu'il n'y a aucun moyen de définir l' CopyToOutputDirectoryélément sans utiliser un script PowerShell (qui ne sera exécuté que dans Visual Studio, pas à partir de l'invite de commande, sur les serveurs de build ou dans autres IDE, et n'est pas pris en charge dans les projets project.json / xproj DNX ) et je préfère utiliser un Linkpour les fichiers plutôt que d'avoir une copie supplémentaire des fichiers dans le projet.

Mise à jour: Bien que cela devrait également fonctionner avec Contentplutôt qu'il Nonene semble qu'il y ait un bogue dans msbuild, les fichiers ne seront donc pas copiés pour référencer les projets plus d'une étape supprimée (par exemple, proj1 -> proj2 -> proj3, proj3 ne recevra pas les fichiers du package NuGet de proj1 mais proj2 le fera).

kjbartel
la source
4
Monsieur, vous êtes un génie! Fonctionne comme un charme. Merci.
MoonStom
Vous vous demandez pourquoi la condition est '$(MSBuildThisFileDirectory)' != '' And HasTrailingSlash('$(MSBuildThisFileDirectory)')requise? Je pensais que le MSBuildThisFileDirectoryest toujours réglé. Quand ce ne serait pas le cas?
kkm
@kkm Honnêtement. Je ne pense pas que ce soit nécessaire. Je ne me souviens même pas d'où je l'ai eu à l'origine.
kjbartel
@kkm J'ai initialement modifié le package nuget System.Data.SQLite et il semble que j'ai laissé cela derrière moi lorsque j'ai supprimé toutes les autres conneries qu'ils incluaient. Fichier de cibles d'origine .
kjbartel
2
@SuperJMN Il y a des jokers ici. N'avez-vous pas remarqué le **\*.dll? C'est copier tous les .dllfichiers dans tous les répertoires. Vous pouvez facilement **\*.*copier toute une arborescence de répertoires.
kjbartel
30

J'ai récemment eu le même problème lorsque j'ai essayé de créer un package EmguCV NuGet comprenant à la fois des assemblys gérés et des liraires partagés non gérés (qui devaient également être placés dans un x86sous - répertoire) qui devaient être copiés automatiquement dans le répertoire de sortie de la construction après chaque build .

Voici une solution que j'ai trouvée, qui ne repose que sur NuGet et MSBuild:

  1. Placez les assemblys gérés dans le /librépertoire du package (partie évidente) et les bibliothèques partagées non gérées et les fichiers associés (par exemple les packages .pdb) dans le /buildsous - répertoire (comme décrit dans la documentation NuGet ).

  2. Renommez toutes les *.dllterminaisons de fichiers non gérées en quelque chose de différent, par exemple *.dl_pour empêcher NuGet de se plaindre du placement d'assemblys présumés au mauvais endroit ( "Problème: Assemblage en dehors du dossier lib." ).

  3. Ajoutez un <PackageName>.targetsfichier personnalisé dans le /buildsous - répertoire avec quelque chose comme le contenu suivant (voir ci-dessous pour une 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>
    

Le .targetsfichier ci-dessus sera injecté sur une installation du package NuGet dans le fichier de projet cible et est responsable de la copie des bibliothèques natives dans le répertoire de sortie.

  • <AvailableItemName Include="NativeBinary" /> ajoute un nouvel élément "Build Action" pour le projet (qui devient également disponible dans la liste déroulante "Build Action" à l'intérieur de Visual Studio).

  • <NativeBinary Include="...ajoute les bibliothèques natives placées dans /build/x86le projet en cours et les rend accessibles à la cible personnalisée qui copie ces fichiers dans le répertoire de sortie.

  • <TargetPath>x86</TargetPath>ajoute des métadonnées personnalisées aux fichiers et indique à la cible personnalisée de copier les fichiers natifs dans le x86sous - répertoire du répertoire de sortie réel.

  • Le <PrepareForRunDependsOn ...bloc ajoute la cible personnalisée à la liste des cibles dont dépend la construction, consultez le fichier Microsoft.Common.targets pour plus de détails.

  • La cible personnalisée,, CopyNativeBinariescontient deux tâches de copie. Le premier est chargé de copier tous les *.dl_fichiers dans le répertoire de sortie tout en remettant leur extension à l'original *.dll. Le second copie simplement le reste (par exemple tous les *.pdbfichiers) au même emplacement. Cela pourrait être remplacé par une tâche de copie unique et un script install.ps1 qui devait renommer tous les *.dl_fichiers *.dllpendant l'installation du package.

Cependant, cette solution ne copierait toujours pas les binaires natifs dans le répertoire de sortie d'un autre projet référençant celui qui inclut initialement le package NuGet. Vous devez également référencer le package NuGet dans votre projet "final".

buygrush
la source
4
" Cependant, cette solution ne copierait toujours pas les binaires natifs dans le répertoire de sortie d'un autre projet faisant référence à celui qui inclut initialement le package NuGet. Vous devez toujours référencer le package NuGet dans votre projet" final ". " Ceci est un show stopper pour moi. Cela signifie généralement que vous devez ajouter le package nuget à plusieurs projets (tels que des tests unitaires), sinon vous êtes DllNotFoundExceptionrenvoyé.
kjbartel
2
un peu drastique pour renommer les fichiers, etc. juste à cause de l'avertissement.
vous pouvez supprimer l'avertissement en l'ajoutant <NoWarn>NU5100</NoWarn>à votre fichier de projet
Florian Koch
29

Voici une alternative qui utilise .targetspour injecter la DLL native dans le projet avec les propriétés suivantes.

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

Le principal avantage de cette technique est que la DLL native est copiée bin/de manière transitoire dans le dossier des projets dépendants .

Voir la mise en page du .nuspecfichier:

Capture d'écran de NuGet Package Explorer

Voici le .targetsfichier:

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

Cela insère le MyNativeLib.dllcomme s'il faisait partie du projet d'origine (mais curieusement, le fichier n'est pas visible dans Visual Studio).

Notez l' <Link>élément qui définit le nom du fichier de destination dans le bin/dossier.

Benoit Blanchon
la source
Fonctionne très bien avec certains fichiers .bat et .ps1 que je dois inclure dans le cadre de mon service Azure - merci :)
Zhaph - Ben Duguid
"(Mais curieusement, le fichier n'est pas visible dans Visual Studio)." - les fichiers de projet sont analysés par VS lui-même AFAIK, donc les éléments ajoutés dans les fichiers .target externes (ou ceux créés dynamiquement lors de l'exécution cible) ne sont pas affichés.
kkm
En quoi est-ce différent de l' autre réponse précédente autre que de passer de Contentà None?
kjbartel
3
wow tu es rapide. de toute façon, si vous choisissez de le faire, vous pourriez au moins demander «en quoi est-ce différent de ma réponse». cet imo serait plus juste que d'éditer la question originale, d'y répondre vous-même et de promouvoir votre réponse dans les commentaires des autres. sans oublier que j'aime personnellement cette réponse particulière mieux que la vôtre - elle est concise,
précise
3
@MaksimSatsikau Vous voudrez peut-être consulter l'historique. J'ai édité la question pour la rendre plus claire, puis j'ai répondu à la question. Cette réponse est venue quelques semaines plus tard et était effectivement une copie. Désolé si j'ai trouvé cela impoli.
kjbartel
19

Si quelqu'un d'autre tombe sur ça.

Le .targetsnom de fichier DOIT être égal à l'ID de package NuGet

Tout le reste ne fonctionnera pas.

Les crédits vont à: https://sushihangover.github.io/nuget-and-msbuild-targets/

J'aurais dû lire plus en détail, comme indiqué ici. Cela m'a pris du temps.

Ajouter une personnalisation <PackageName>.targets

DirtyLittleHelper
la source
3
vous sauvez ma journée entière!
zheng yu
1
Vous avez résolu un problème d'une semaine avec autre chose. Merci à vous et à cette page github.
Glenn Watson
13

Il est un peu tard mais j'ai créé un package nuget spécialement pour cela.

L'idée est d'avoir un dossier spécial supplémentaire dans votre package nuget. Je suis sûr que vous connaissez déjà Lib et Content. Le package nuget que j'ai créé recherche un dossier nommé Output et copiera tout ce qui s'y trouve dans le dossier de sortie des projets.

La seule chose que vous devez faire est d'ajouter une dépendance nuget au package http://www.nuget.org/packages/Baseclass.Contrib.Nuget.Output/

J'ai écrit un article de blog à ce sujet: http://www.baseclass.ch/blog/Lists/Beitraege/Post.aspx?ID=6&mobile=0

Daniel Romero
la source
C'est génial! Cependant, cela ne fonctionne que dans le projet en cours. Si le projet est une "Bibliothèque de classes" et que vous souhaitez ajouter comme dépendance à une "Application Web" par exemple, les DLL ne seront pas construites dans l'application Web! Ma «solution rapide» est la suivante: créer un NuGet pour votre bibliothèque et appliquer à la bibliothèque de classes, et créer un autre Nuget pour les dépendances (dll dans ce cas) et appliquer à WebApplication. La meilleure solution pour cela?
Wagner Leonardi
Vous semblez avoir créé ce projet pour .NET 4.0 (Windows) uniquement. Envisagez-vous de le mettre à jour pour prendre également en charge les bibliothèques de classes portables?
Ani
1

Il existe une solution pure C # que je trouve plutôt facile à utiliser et je n'ai pas à me soucier des limitations de NuGet. Suivez ces étapes:

Incluez la bibliothèque native dans votre projet et définissez sa propriété Build Action sur Embedded Resource .

Collez le code suivant dans la classe où vous PInvoke cette bibliothèque.

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

Appelez cette méthode à partir du constructeur statique comme suit UnpackNativeLibrary("win32");et elle décompressera la bibliothèque sur le disque juste avant que vous en ayez besoin. Bien entendu, vous devez vous assurer que vous disposez des autorisations d'écriture sur cette partie du disque.

Ondrej Janacek
la source
1

C'est une vieille question, mais j'ai le même problème maintenant, et j'ai trouvé un revirement un peu délicat mais très simple et efficace: créer dans le dossier Contenu standard Nuget la structure suivante avec un sous-dossier pour chaque configuration:

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

Lorsque vous compressez le fichier nuspec, vous recevrez le message suivant pour chaque bibliothèque native dans les dossiers Debug et Release:

Problème: assemblage en dehors du dossier lib. Description: L'assembly 'Content \ Bin \ Debug \ ??????. Dll' n'est pas dans le dossier 'lib' et ne sera donc pas ajouté comme référence lorsque le package est installé dans un projet. Solution: déplacez-le dans le dossier «lib» s'il doit être référencé.

Nous n'avons pas besoin d'une telle "solution" car c'est juste notre objectif: que les bibliothèques natives ne soient pas ajoutées en tant que références aux assemblys NET.

Les avantages sont:

  1. Solution simple sans scripts encombrants avec des effets étranges difficiles à réinitialiser lors de la désinstallation du package.
  2. Nuget gère les bibliothèques natives comme tout autre contenu lors de l'installation et de la désinstallation.

Les inconvénients sont:

  1. Vous avez besoin d'un dossier pour chaque configuration (mais en général, il n'y en a que deux: Debug et Release, et si vous avez un autre contenu qui doit être installé dans chaque dossier de configuration, c'est soit la voie à suivre)
  2. Les bibliothèques natives doivent être dupliquées dans chaque dossier de configuration (mais si vous avez différentes versions des bibliothèques natives pour chaque configuration, c'est la voie à suivre)
  3. Les avertissements pour chaque dll native dans chaque dossier (mais comme je l'ai dit, ils sont envoyés au créateur du package au moment du pack, pas à l'utilisateur du package au moment de l'installation de VS)
SERWare
la source
0

Je ne peux pas résoudre votre problème exact, mais je peux vous donner une suggestion.

Votre exigence clé est: "Et ne pas enregistrer automatiquement la référence" .....

Vous devrez donc vous familiariser avec les «éléments de solution»

Voir référence ici:

Ajout d'éléments au niveau de la solution dans un package NuGet

Vous devrez écrire du vaudou PowerShell pour obtenir la copie de votre dll native dans sa maison (encore une fois, parce que vous ne voulez PAS que le vaudou de référence automatique se déclenche)

Voici un fichier ps1 que j'ai écrit ..... pour mettre des fichiers dans un dossier de références tiers.

Il y en a assez pour que vous compreniez comment copier votre dll natif dans une "maison" ... sans avoir à recommencer à zéro.

Encore une fois, ce n'est pas un coup direct, mais c'est mieux que rien.

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" 
grenadeCoder
la source
-2

Mettez c'est le dossier de contenu

la commande le nuget pack [projfile].csprojfera automatiquement pour vous si vous marquez les fichiers comme contenu.

puis éditez le fichier projet comme mentionné ici en ajoutant l'élément ItemGroup & NativeLibs & None

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

travaillé pour moi

Sharon Salmon
la source