Agregue archivos nativos del paquete NuGet al directorio de salida del proyecto

126

Estoy tratando de crear el paquete NuGet para un ensamblado .Net que hace pinvoke a un win32 dll nativo. Necesito empacar tanto el ensamblado como el dll nativo con el ensamblado agregado a las referencias del proyecto (no hay problema en esta parte) y el dll nativo debe copiarse en el directorio de salida del proyecto o en algún otro directorio relativo.

Mis preguntas son:

  1. ¿Cómo empaco el dll nativo sin que Visual Studio intente agregarlo a la lista de referencias?
  2. ¿Tengo que escribir un archivo install.ps1 para copiar el dll nativo? Si es así, ¿cómo puedo acceder al contenido del paquete para copiarlo?
AlonFStackoverflow
fuente
1
Hay soporte para bibliotecas específicas de tiempo de ejecución / arquitectura, pero falta la documentación de la característica y parece ser específica de UWP. docs.microsoft.com/en-us/nuget/create-packages/…
Wouter

Respuestas:

131

El uso del Copydestino en el archivo de destino para copiar las bibliotecas requeridas no copiará esos archivos a otros proyectos que hagan referencia al proyecto, lo que dará como resultado un DllNotFoundException. Sin embargo, esto se puede hacer con un archivo de objetivos mucho más simple, utilizando un Noneelemento, ya que MSBuild copiará todos los Nonearchivos a proyectos de referencia.

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

Agregue el archivo de destino al builddirectorio del paquete nuget junto con las bibliotecas nativas requeridas. El archivo de objetivos incluirá todos los dllarchivos en todos los directorios secundarios del builddirectorio. Así que añadir una x86y x64versión de una biblioteca nativa utilizada por un Any CPUensamblado administrado que terminaría con una estructura de directorios similar a la siguiente:

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

Lo mismo x86y los x64directorios se crearán en el directorio de salida del proyecto cuando se construyan. Si no necesita subdirectorios, el **y el %(RecursiveDir)se pueden eliminar y, en su lugar, incluir los archivos necesarios en el builddirectorio directamente. Otros archivos de contenido necesarios también se pueden agregar de la misma manera.

Los archivos agregados como Noneen el archivo de destino no se mostrarán en el proyecto cuando se abran en Visual Studio. Si se pregunta por qué no uso la Contentcarpeta en el nupkg es porque no hay forma de configurar el CopyToOutputDirectoryelemento sin usar un script de PowerShell (que solo se ejecutará dentro de Visual Studio, no desde el símbolo del sistema, en los servidores de compilación o en otros IDEs, y no es compatible con proyectos project.json / xproj DNX ) y prefiero usar a Linkpara los archivos en lugar de tener una copia adicional de los archivos dentro del proyecto.

Actualización: aunque esto también debería funcionar en Contentlugar de Noneparecer que hay un error en msbuild, por lo que los archivos no se copiarán a proyectos de referencia que se eliminen más de un paso (por ejemplo, proj1 -> proj2 -> proj3, proj3 no obtendrá los archivos del paquete NuGet de proj1 pero proj2 lo hará).

kjbartel
fuente
44
Señor, usted es un genio! Funciona de maravilla. Gracias.
MoonStom
¿Se pregunta por qué se '$(MSBuildThisFileDirectory)' != '' And HasTrailingSlash('$(MSBuildThisFileDirectory)')requiere la condición ? Pensé que el MSBuildThisFileDirectorysiempre está configurado. ¿Cuándo ese no sería el caso?
kkm
@kkm Honestamente. No creo que sea necesario. Ni siquiera puedo recordar de dónde lo obtuve originalmente.
kjbartel
@kkm Originalmente modifiqué el paquete nuget System.Data.SQLite y parece que lo dejé atrás cuando eliminé todas las demás basura que incluían. Archivo de objetivos original .
kjbartel
2
@SuperJMN Hay comodines allí. ¿No te diste cuenta de la **\*.dll? Eso es copiar todos los .dllarchivos en todos los directorios. Podría hacer fácilmente **\*.*para copiar un árbol de directorios completo.
kjbartel
30

Recientemente tuve el mismo problema cuando intenté construir un paquete Nuguet de EmguCV que incluye tanto ensamblados administrados como bibliotecas compartidas no administradas (que también tuvieron que colocarse en un x86 subdirectorio) que tuvieron que copiarse automáticamente al directorio de salida de compilación después de cada compilación .

Aquí hay una solución que se me ocurrió, que se basa solo en NuGet y MSBuild:

  1. Coloque los ensamblados administrados en el /libdirectorio del paquete (parte obvia) y las bibliotecas compartidas no administradas y los archivos relacionados (por ejemplo, paquetes .pdb) en el /buildsubdirectorio (como se describe en los documentos de NuGet ).

  2. Cambie el nombre de todas las *.dllterminaciones de archivos no administradas a algo diferente, por ejemplo, *.dl_para evitar que NuGet se queje de que los supuestos ensamblajes se coloquen en un lugar incorrecto ( "Problema: ensamblaje fuera de la carpeta lib" ).

  3. Agregue un <PackageName>.targetsarchivo personalizado en el /buildsubdirectorio con algo como los siguientes contenidos (consulte la descripción a continuación):

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

El .targetsarchivo anterior se inyectará en una instalación del paquete NuGet en el archivo de proyecto de destino y es responsable de copiar las bibliotecas nativas en el directorio de salida.

  • <AvailableItemName Include="NativeBinary" /> agrega un nuevo elemento "Build Action" para el proyecto (que también está disponible en el menú desplegable "Build Action" dentro de Visual Studio).

  • <NativeBinary Include="...agrega las bibliotecas nativas ubicadas en el /build/x86proyecto actual y las hace accesibles para el destino personalizado que copia esos archivos en el directorio de salida.

  • <TargetPath>x86</TargetPath>agrega metadatos personalizados a los archivos y le dice al objetivo personalizado que copie los archivos nativos en el x86subdirectorio del directorio de salida real.

  • El <PrepareForRunDependsOn ...bloque agrega el objetivo personalizado a la lista de objetivos de los que depende la compilación; consulte el archivo Microsoft.Common.targets para obtener más información.

  • El objetivo personalizado CopyNativeBinaries, contiene dos tareas de copia. El primero es responsable de copiar cualquier *.dl_archivo al directorio de salida mientras cambia su extensión de nuevo al original*.dll . El segundo simplemente copia el resto (por ejemplo, cualquier *.pdbarchivo) a la misma ubicación. Esto podría ser reemplazado por una tarea de copia única y un script install.ps1 que tuvo que cambiar el nombre de todos los *.dl_archivos *.dlldurante la instalación del paquete.

Sin embargo, esta solución aún no copiaría los binarios nativos al directorio de salida de otro proyecto que haga referencia al que inicialmente incluye el paquete NuGet. Todavía tiene que hacer referencia al paquete NuGet en su proyecto "final" también.

buygrush
fuente
44
" Sin embargo, esta solución todavía no copiaría los binarios nativos en el directorio de salida de otro proyecto que haga referencia al que inicialmente incluye el paquete NuGet. Todavía tiene que hacer referencia al paquete NuGet en su proyecto" final "también " . Muestra tapón para mí. Por lo general, significa que debe agregar el paquete nuget a varios proyectos (como pruebas unitarias), de lo contrario, se verá afectado DllNotFoundException.
kjbartel
2
un poco drástico cambiar el nombre de los archivos, etc. solo por la advertencia.
puede eliminar la advertencia agregando <NoWarn>NU5100</NoWarn>a su archivo de proyecto
Florian Koch
29

Aquí hay una alternativa que usa .targetspara inyectar la DLL nativa en el proyecto con las siguientes propiedades.

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

El principal beneficio de esta técnica es que la DLL nativa se copia en la bin/carpeta de proyectos dependientes transitiva.

Vea el diseño del .nuspecarchivo:

Captura de pantalla de NuGet Package Explorer

Aquí está el .targetsarchivo:

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

Esto inserta el MyNativeLib.dll como si fuera parte del proyecto original (pero curiosamente el archivo no es visible en Visual Studio).

Observe el <Link>elemento que establece el nombre del archivo de destino en la bin/carpeta.

Benoit Blanchon
fuente
Funciona de maravilla con algunos archivos .bat y .ps1 que necesito incluir como parte de mi servicio Azure - gracias :)
Zhaph - Ben Duguid
"(Pero curiosamente el archivo no es visible en Visual Studio)". - los archivos del proyecto son analizados por VS AFAIK, por lo que los elementos agregados en archivos externos .target (o aquellos creados dinámicamente en la ejecución de destino) no se muestran.
kkm
¿En qué se diferencia esto de la otra respuesta anterior además de cambiar de Contenta None?
kjbartel
3
wow eres rápido de todos modos, si eliges hacerlo, al menos podrías preguntar 'en qué se diferencia esto de mi respuesta'. eso sería más justo que editar la pregunta original, responderla usted mismo y luego promover su respuesta en los comentarios de otras personas. sin mencionar que personalmente me gusta esta respuesta en particular mejor que la tuya - es conciso, al grano y más fácil de leer
Maksim Satsikau
3
@MaksimSatsikau Es posible que desee ver el historial. Edité la pregunta para aclararla, luego respondí la pregunta. Esta respuesta llegó un par de semanas después y fue efectivamente una copia. Lo siento si me pareció grosero.
kjbartel
19

Si alguien más se topa con esto.

El .targetsnombre del archivo DEBE ser igual al Id. Del paquete NuGet

Cualquier otra cosa no funcionará.

Los créditos van a: https://sushihangover.github.io/nuget-and-msbuild-targets/

Debería haber leído más a fondo, ya que en realidad se señala aquí. Me llevó años ...

Agregar una costumbre <PackageName>.targets

DirtyLittleHelper
fuente
3
me salvas todo el día!
zheng yu
1
Arreglaste un problema de una semana con algo más. Gracias a ti y a esa página de Github.
Glenn Watson
13

Es un poco tarde, pero he creado un paquete nuget exactamente para eso.

La idea es tener una carpeta especial adicional en su paquete nuget. Estoy seguro de que ya conoces Lib y Content. El paquete nuget que he creado busca una carpeta llamada Salida y copiará todo lo que está allí a la carpeta de salida de proyectos.

Lo único que debe hacer es agregar una dependencia nuget al paquete http://www.nuget.org/packages/Baseclass.Contrib.Nuget.Output/

He escrito una publicación de blog al respecto: http://www.baseclass.ch/blog/Lists/Beitraege/Post.aspx?ID=6&mobile=0

Daniel Romero
fuente
¡Eso es genial! Sin embargo, esto solo funciona en el proyecto actual. Si el proyecto es una "Biblioteca de clases" y desea agregar como dependencia a una "Aplicación web", por ejemplo, ¡las DLL no se construirán en la aplicación web! Mi "solución rápida" es: crear un NuGet para su biblioteca, y aplicar a la Biblioteca de clases, y crear otro Nuget para las dependencias (dlls en este caso) y aplicar a la aplicación web. ¿Alguna mejor solución para esto?
Wagner Leonardi
Parece que ha creado este proyecto solo para .NET 4.0 (Windows). ¿Planea actualizarlo para admitir bibliotecas de clases portátiles también?
Ani
1

Hay una solución C # pura que encuentro bastante fácil de usar y no tengo que molestarme con las limitaciones de NuGet. Sigue estos pasos:

Incluya la biblioteca nativa en su proyecto y establezca su propiedad Build Action en Embedded Resource.

Pega el siguiente código en la clase donde invocas esta biblioteca.

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

Llame a este método desde el constructor estático de la siguiente manera UnpackNativeLibrary("win32");y desempaquetará la biblioteca en el disco justo antes de que lo necesite. Por supuesto, debe asegurarse de tener permisos de escritura en esa parte del disco.

Ondrej Janacek
fuente
1

Esta es una vieja pregunta, pero ahora tengo el mismo problema, y ​​encontré un cambio que es un poco complicado pero muy simple y efectivo: cree en la carpeta de Contenido estándar de Nuget la siguiente estructura con una subcarpeta para cada configuración:

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

Cuando empaque el archivo nuspec, recibirá el siguiente mensaje para cada biblioteca nativa en las carpetas Debug and Release:

Problema: Asamblea fuera de la carpeta lib. Descripción: el ensamblado 'Content \ Bin \ Debug \ ??????. Dll' no está dentro de la carpeta 'lib' y, por lo tanto, no se agregará como referencia cuando el paquete se instale en un proyecto. Solución: muévala a la carpeta 'lib' si debería hacer referencia a ella.

No necesitamos esa "solución" porque este es solo nuestro objetivo: que las bibliotecas nativas no se agreguen como referencias de ensamblados NET.

Las ventajas son:

  1. Solución simple sin scripts engorrosos con efectos extraños que son difíciles de restablecer al desinstalar el paquete.
  2. Nuget administra las bibliotecas nativas como cualquier otro contenido al instalar y desinstalar.

Las desventajas son:

  1. Necesita una carpeta para cada configuración (pero generalmente solo hay dos: Depurar y Liberar, y si tiene otro contenido que debe instalarse en cada carpeta de configuración, este es el camino a seguir)
  2. Las bibliotecas nativas deben estar duplicadas en cada carpeta de configuración (pero si tiene diferentes versiones de las bibliotecas nativas para cada configuración, este es el camino a seguir)
  3. Las advertencias para cada dll nativo en cada carpeta (pero como dije, se emiten advertencias para el creador del paquete en el momento del paquete, no para el usuario del paquete en el momento de la instalación VS)
SERWare
fuente
0

No puedo resolver tu problema exacto, pero puedo darte una sugerencia.

Su requisito clave es: "Y que no se registre automáticamente la referencia" .....

Por lo tanto, deberá familiarizarse con los "elementos de solución"

Ver referencia aquí:

Agregar elementos a nivel de solución en un paquete NuGet

Tendrá que escribir un poco de vudú de PowerShell para obtener la copia de su dll nativo en su hogar (de nuevo, porque NO desea que se active el vudú de referencia automática)

Aquí hay un archivo ps1 que escribí ... para colocar archivos en una carpeta de referencias de terceros.

Hay suficiente para que descubras cómo copiar tu dll nativo a algún "hogar" ... sin tener que empezar desde cero.

De nuevo, no es un golpe directo, pero es mejor que nada.

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

Ponlo es la carpeta de contenido

El comando nuget pack [projfile].csproj lo hará por usted automáticamente si marca los archivos como contenido.

luego edite el archivo del proyecto como se menciona aquí agregando el elemento ItemGroup & NativeLibs & None

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

trabajó para mi

Sharon Salmon
fuente