Aggiungi file nativi dal pacchetto NuGet alla directory di output del progetto


126

Sto cercando di creare un pacchetto NuGet per un assembly .Net che fa pinvoke a una DLL nativa win32. Ho bisogno di comprimere sia l'assembly che la dll nativa con l'assembly aggiunto ai riferimenti del progetto (nessun problema in questa parte) e la dll nativa dovrebbe essere copiata nella directory di output del progetto o in qualche altra directory relativa.

Le mie domande sono:

  1. Come impacchettare la DLL nativa senza Visual Studio che prova ad aggiungerla all'elenco dei riferimenti?
  2. Devo scrivere un file install.ps1 per copiare la dll nativa? In tal caso, come posso accedere al contenuto del pacchetto per copiarlo?

1
Esiste il supporto per librerie specifiche di runtime / architettura, ma manca la documentazione relativa alle funzionalità e sembra essere UWP specifica. docs.microsoft.com/en-us/nuget/create-packages/…
Wouter

Risposte:


131

L'uso della Copydestinazione nel file target per copiare le librerie richieste non copierà quei file in altri progetti che fanno riferimento al progetto, risultando in a DllNotFoundException. Questo può essere fatto con un file target molto più semplice, usando un Noneelemento, poiché MSBuild copierà tutti i Nonefile in progetti di riferimento.

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

Aggiungi il file target alla builddirectory del pacchetto nuget insieme alle librerie native richieste. Il file target includerà tutti i dllfile in tutte le directory figlio della builddirectory. Quindi, per aggiungere una x86e una x64versione di una libreria nativa utilizzata da un Any CPUassembly gestito, si otterrebbe una struttura di directory simile alla seguente:

  • costruire
    • X 86
      • NativeLib.dll
      • NativeLibDependency.dll
    • x64
      • NativeLib.dll
      • NativeLibDependency.dll
    • MyNugetPackageID.targets
  • lib
    • net40
      • ManagedAssembly.dll

Lo stesso x86e le x64directory verranno create nella directory di output del progetto quando vengono create. Se non hai bisogno di sottodirectory, allora il **e il %(RecursiveDir)possono essere rimossi e invece includere direttamente i file richiesti nella builddirectory. Altri file di contenuto richiesti possono anche essere aggiunti allo stesso modo.

I file aggiunti come Nonenel file target non verranno mostrati nel progetto quando sono aperti in Visual Studio. Se ti stai chiedendo perché non uso la Contentcartella in nupkg è perché non c'è modo di impostare l' CopyToOutputDirectoryelemento senza usare uno script PowerShell (che verrà eseguito solo all'interno di Visual Studio, non dal prompt dei comandi, sui server di compilazione o in altri IDE e non è supportato nei progetti DNX project.json / xproj ) e preferisco utilizzare un Linkfile piuttosto che avere una copia aggiuntiva dei file all'interno del progetto.

Aggiornamento: anche se questo dovrebbe funzionare anche Contentpiuttosto che Nonesembra che ci sia un bug in msbuild in modo che i file non vengano copiati nei progetti di riferimento più di un passaggio rimosso (ad esempio proj1 -> proj2 -> proj3, proj3 non otterrà i file dal pacchetto NuGet di proj1 ma proj2 lo farà).


4
Signore, sei un genio! Funziona come un fascino. Grazie.
MoonStom,

Ti chiedi perché è '$(MSBuildThisFileDirectory)' != '' And HasTrailingSlash('$(MSBuildThisFileDirectory)')richiesta la condizione ? Ho pensato che MSBuildThisFileDirectoryè sempre impostato. Quando non sarebbe così?
kkm,

@kkm Onestamente. Non penso sia necessario. Non riesco nemmeno a ricordare da dove l'ho preso in origine.
kjbartel,

@kkm Ho originariamente modificato il pacchetto nuget System.Data.SQLite e sembra che me ne sia lasciato indietro quando ho rimosso tutte le altre schifezze che includevano. File target originale .
kjbartel,

2
@SuperJMN Ci sono caratteri jolly lì. Non hai notato il **\*.dll? Questo è copiare tutti i .dllfile in tutte le directory. Si potrebbe facilmente fare **\*.*per copiare un intero albero di directory.
kjbartel,

30

Di recente ho avuto lo stesso problema quando ho provato a creare un pacchetto NuGet EmguCV che includeva sia assembly gestiti che lirari condivisi non gestiti (che dovevano anche essere collocati in una x86sottodirectory) che dovevano essere copiati automaticamente nella directory di output della build dopo ogni build .

Ecco una soluzione che mi è venuta in mente, che si basa solo su NuGet e MSBuild:

  1. Posizionare gli assembly gestiti nella /libdirectory del pacchetto (parte ovvia) e le librerie condivise non gestite e i file correlati (ad es. Pacchetti .pdb) nella /buildsottodirectory (come descritto nei documenti NuGet ).

  2. Rinominare tutte le *.dllterminazioni di file non gestite in qualcosa di diverso, ad esempio *.dl_per impedire a NuGet di lamentarsi di presunti assembly posizionati in un posto errato ( "Problema: assembly fuori dalla cartella lib." ).

  3. Aggiungi un <PackageName>.targetsfile personalizzato nella /buildsottodirectory con qualcosa come il seguente contenuto (vedi sotto per una descrizione):

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

Il .targetsfile sopra verrà iniettato su un'installazione del pacchetto NuGet nel file di progetto di destinazione ed è responsabile della copia delle librerie native nella directory di output.

  • <AvailableItemName Include="NativeBinary" /> aggiunge un nuovo elemento "Build Action" per il progetto (che diventa disponibile anche nel menu a discesa "Build Action" all'interno di Visual Studio).

  • <NativeBinary Include="...aggiunge le librerie native collocate nel /build/x86progetto corrente e le rende accessibili alla destinazione personalizzata che copia quei file nella directory di output.

  • <TargetPath>x86</TargetPath>aggiunge metadati personalizzati ai file e indica al target personalizzato di copiare i file nativi nella x86sottodirectory della directory di output effettiva.

  • Il <PrepareForRunDependsOn ...blocco aggiunge il target personalizzato all'elenco dei target da cui dipende la build, vedere il file Microsoft.Common.targets per i dettagli.

  • La destinazione personalizzata CopyNativeBinaries, contiene due attività di copia. Il primo è responsabile della copia di tutti i *.dl_file nella directory di output mentre la loro estensione viene ripristinata sull'originale *.dll. Il secondo copia semplicemente il resto (ad esempio qualsiasi *.pdbfile) nella stessa posizione. Questo potrebbe essere sostituito da una singola attività di copia e da uno script install.ps1 che ha dovuto rinominare tutti i *.dl_file *.dlldurante l'installazione del pacchetto.

Tuttavia, questa soluzione non copierebbe ancora i file binari nativi nella directory di output di un altro progetto che fa riferimento a quello che inizialmente include il pacchetto NuGet. Devi ancora fare riferimento al pacchetto NuGet anche nel tuo progetto "finale".


4
" Tuttavia, questa soluzione continua a non copiare i file binari nativi nella directory di output di un altro progetto che fa riferimento a quello che inizialmente include il pacchetto NuGet. Devi comunque fare riferimento anche al pacchetto NuGet nel tuo progetto" finale ". " mostra tappo per me. Di solito significa che è necessario aggiungere il pacchetto nuget a più progetti (come i test unitari), altrimenti verrai DllNotFoundExceptionlanciato.
kjbartel,

2
un po 'drastico per rinominare i file ecc solo a causa dell'avvertimento.

puoi rimuovere l'avviso aggiungendolo <NoWarn>NU5100</NoWarn>al tuo file di progetto
Florian Koch,

29

Ecco un'alternativa che utilizza il .targetsper iniettare la DLL nativa nel progetto con le seguenti proprietà.

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

Il vantaggio principale di questa tecnica è che la DLL nativa viene copiata in modo transitorio nella bin/cartella dei progetti dipendenti .

Vedi il layout del .nuspecfile:

Schermata di NuGet Package Explorer

Ecco il .targetsfile:

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

Ciò inserisce il file MyNativeLib.dllcome se fosse parte del progetto originale (ma curiosamente il file non è visibile in Visual Studio).

Notare l' <Link>elemento che imposta il nome del file di destinazione nella bin/cartella.


Funziona a meraviglia con alcuni file .bat e .ps1 che devo includere come parte del mio servizio Azure - grazie :)
Zhaph - Ben Duguid,

"(Ma curiosamente il file non è visibile in Visual Studio)." - i file di progetto vengono analizzati da VS stesso AFAIK, quindi gli elementi aggiunti in file .target esterni (o quelli creati dinamicamente nell'esecuzione di destinazione) non vengono visualizzati.
kkm,

In che modo questa è diversa dall'altra prima risposta diversa dal passaggio da Contenta a None?
kjbartel,

3
wow sei veloce. in ogni caso, se scegli di farlo, potresti almeno chiedere "come è diverso dalla mia risposta". che imo sarebbe più giusto che modificare la domanda originale, rispondendo a te stesso e quindi promuovendo la tua risposta nei commenti di altre persone. per non parlare del fatto che personalmente mi piace questa risposta in particolare meglio della tua - è concisa,
precisa

3
@MaksimSatsikau Potresti voler guardare la storia. Ho modificato la domanda per renderla più chiara, quindi ho risposto alla domanda. Questa risposta è arrivata un paio di settimane dopo ed è stata effettivamente una copia. Scusa se l'ho trovato maleducato.
kjbartel,

19

Se qualcun altro si imbatte in questo.

Il .targetsnome file DEVE essere uguale all'ID pacchetto NuGet

Nient'altro non funzionerà.

I crediti vanno a: https://sushihangover.github.io/nuget-and-msbuild-targets/

Avrei dovuto leggere più a fondo come è stato effettivamente notato qui. Mi ci sono voluti anni ..

Aggiungi un'abitudine <PackageName>.targets


3
mi salvi tutto il giorno!
zheng yu,

1
Hai risolto un problema di una settimana con qualcos'altro. Grazie a te e alla pagina di github.
Glenn Watson,

13

È un po 'tardi ma per questo ho creato un pacchetto nuget.

L'idea è di avere una cartella speciale aggiuntiva nel pacchetto nuget. Sono sicuro che conosci già Lib e contenuti. Il pacchetto nuget che ho creato cerca una cartella denominata Output e copia tutto ciò che è presente nella cartella di output dei progetti.

L'unica cosa che devi fare è aggiungere una dipendenza nuget al pacchetto http://www.nuget.org/packages/Baseclass.Contrib.Nuget.Output/

Ho scritto un post sul blog a riguardo: http://www.baseclass.ch/blog/Lists/Beitraege/Post.aspx?ID=6&mobile=0


È fantastico! Tuttavia, questo funziona solo nel progetto corrente. Se il progetto è una "Libreria di classi" e si desidera aggiungere come dipendenza a una "Applicazione Web" per es., Le DLL non verranno create nell'applicazione Web! La mia "soluzione rapida" è: creare un NuGet per la libreria e applicare alla libreria di classi e creare un altro Nuget per le dipendenze (dll in questo caso) e applicare a WebApplication. Qual è la migliore soluzione per questo?
Wagner Leonardi

Sembra che tu abbia creato questo progetto solo per .NET 4.0 (Windows). Prevedi di aggiornarlo per supportare anche le librerie di classi portatili?
Ani,

1

C'è una soluzione C # pura che trovo piuttosto facile da usare e non devo preoccuparmi delle limitazioni di NuGet. Segui questi passi:

Includere la libreria nativa nel progetto e impostare la proprietà Build Action su Embedded Resource.

Incollare il codice seguente nella classe in cui si PInvoke questa libreria.

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

Chiama questo metodo dal costruttore statico come segue UnpackNativeLibrary("win32");e decomprimerà la libreria su disco appena prima di averne bisogno. Ovviamente, devi essere sicuro di disporre delle autorizzazioni di scrittura per quella parte del disco.


1

Questa è una vecchia domanda, ma ora ho lo stesso problema e ho trovato un'inversione di tendenza un po 'complicata ma molto semplice ed efficace: creare nella cartella Contenuto standard Nuget la seguente struttura con una sottocartella per ogni configurazione:

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

Quando comprimi il file nuspec, riceverai il seguente messaggio per ogni libreria nativa nelle cartelle Debug e Release:

Problema: assembly all'esterno della cartella lib. Descrizione: L'assembly 'Content \ Bin \ Debug \ ??????. Dll' non si trova all'interno della cartella 'lib' e quindi non verrà aggiunto come riferimento quando il pacchetto viene installato in un progetto. Soluzione: spostarlo nella cartella 'lib' se si deve fare riferimento.

Non abbiamo bisogno di tale "soluzione" perché questo è solo il nostro obiettivo: che le librerie native non vengano aggiunte come riferimenti a NET Assembly.

I vantaggi sono:

  1. Soluzione semplice senza script ingombranti con strani effetti che sono difficili da ripristinare alla disinstallazione del pacchetto.
  2. Nuget gestisce le librerie native come qualsiasi altro contenuto durante l'installazione e la disinstallazione.

Gli svantaggi sono:

  1. È necessaria una cartella per ogni configurazione (ma in genere ce ne sono solo due: Debug e versione e se si dispone di altro contenuto che deve essere installato in ciascuna cartella di configurazione, questa è la strada da percorrere)
  2. Le librerie native devono essere duplicate in ciascuna cartella di configurazione (ma se si dispone di versioni diverse delle librerie native per ciascuna configurazione, questa è la strada da percorrere)
  3. Gli avvisi per ogni DLL nativa in ogni cartella (ma, come ho detto, sono avvertiti al creatore del pacchetto al momento del pacchetto, non all'utente del pacchetto al momento dell'installazione di VS)

0

Non riesco a risolvere il tuo esatto problema, ma posso darti un suggerimento.

Il requisito chiave è: "E non è necessario registrare automaticamente il riferimento" .....

Quindi dovrai familiarizzare con gli "elementi della soluzione"

Vedi riferimento qui:

Aggiunta di elementi a livello di soluzione in un pacchetto NuGet

Dovrai scrivere un voodoo PowerShell per ottenere la copia della tua DLL nativa nella sua home (di nuovo, perché NON vuoi che il voodoo con aggiunta automatica di riferimento venga attivato)

Ecco un file ps1 che ho scritto ..... per mettere i file in una cartella di riferimenti di terze parti.

C'è abbastanza lì per farti capire come copiare la tua dll nativa in qualche "casa" ... senza dover ricominciare da zero.

Ancora una volta, non è un colpo diretto, ma è meglio di niente.

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" 

-2

Mettilo è la cartella del contenuto

il comando nuget pack [projfile].csprojlo farà automaticamente se si contrassegnano i file come contenuti.

quindi modifica il file di progetto come indicato qui aggiungendo ItemGroup & NativeLibs & None element

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

ha funzionato per me

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.