Dodaj pliki natywne z pakietu NuGet do katalogu wyjściowego projektu

126

Próbuję utworzyć pakiet NuGet dla zestawu .Net, który wykonuje pinvoke do natywnej biblioteki dll win32. Muszę spakować zarówno zestaw, jak i natywną bibliotekę dll z zestawem dodanym do odwołań do projektu (nie ma problemu w tej części), a natywną bibliotekę dll należy skopiować do katalogu wyjściowego projektu lub innego katalogu względnego.

Moje pytania to:

  1. Jak spakować natywną bibliotekę dll bez próby dodania jej do listy referencji przez Visual Studio?
  2. Czy muszę napisać plik install.ps1, aby skopiować natywną bibliotekę dll? Jeśli tak, jak mogę uzyskać dostęp do zawartości pakietu w celu jego skopiowania?
AlonFStackoverflow
źródło
1
Dostępna jest obsługa bibliotek specyficznych dla środowiska uruchomieniowego / architektury, ale brakuje dokumentacji funkcji i wydaje się, że jest ona specyficzna dla platformy UWP. docs.microsoft.com/en-us/nuget/create-packages/ ...
Wouter

Odpowiedzi:

131

Użycie elementu Copydocelowego w pliku docelowym do skopiowania wymaganych bibliotek nie spowoduje skopiowania tych plików do innych projektów, które odwołują się do projektu, co spowoduje utworzenie pliku DllNotFoundException. Można to zrobić za pomocą znacznie prostszego pliku docelowego, używając Noneelementu, ponieważ program MSBuild skopiuje wszystkie Nonepliki do projektów odwołujących się.

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

Dodaj plik docelowy do buildkatalogu pakietu nuget wraz z wymaganymi bibliotekami natywnymi. Plik docelowy będzie zawierał wszystkie dllpliki we wszystkich katalogach podrzędnych buildkatalogu. Aby więc dodać wersję x86i x64wersję biblioteki natywnej używanej przez zestaw Any CPUzarządzany, otrzymamy strukturę katalogów podobną do następującej:

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

To samo x86i x64katalogi zostaną utworzone w katalogu wyjściowym projektu po zbudowaniu. Jeśli nie trzeba podkatalogi wtedy **i %(RecursiveDir)mogą być usunięte i zamiast zawierać wymagane pliki wbuild katalogu bezpośrednio. W ten sam sposób można dodać inne wymagane pliki zawartości.

Pliki dodane Nonew pliku docelowym nie będą wyświetlane w projekcie po otwarciu w programie Visual Studio. Jeśli zastanawiasz się, dlaczego nie używam Contentfolderu w nupkg, to dlatego, że nie ma możliwości ustawienia CopyToOutputDirectoryelementu bez użycia skryptu PowerShell (który będzie uruchamiany tylko w programie Visual Studio, a nie z wiersza polecenia, na serwerach kompilacji lub w inne IDE i nie jest obsługiwane w projektach project.json / xproj DNX ) i wolę używać a Linkdo plików, zamiast mieć dodatkową kopię plików w projekcie.

Aktualizacja: Chociaż powinno to również działać, Contenta Nonenie wydaje się, że jest błąd w msbuild, więc pliki nie będą kopiowane do projektów odwołujących się więcej niż jeden krok usunięty (np. Proj1 -> proj2 -> proj3, proj3 nie pobierze plików z pakietu NuGet proj1, ale proj2 będzie).

kjbartel
źródło
4
Sir, jest pan geniuszem! Działa jak marzenie. Dzięki.
MoonStom
Zastanawiasz się, dlaczego ten stan jest '$(MSBuildThisFileDirectory)' != '' And HasTrailingSlash('$(MSBuildThisFileDirectory)')wymagany? Myślałem, że MSBuildThisFileDirectoryjest zawsze ustawione. Kiedy tak by się nie stało?
kkm
@kkm Szczerze. Myślę, że to nie jest potrzebne. Nie pamiętam nawet, skąd go wzięłam.
kjbartel
@kkm Pierwotnie zmodyfikowałem pakiet nuget System.Data.SQLite i wydaje się, że zostawiłem go, kiedy usunąłem wszystkie inne bzdury, które zawierały. Oryginalny plik celów .
kjbartel
2
@SuperJMN Tam są symbole wieloznaczne. Nie zauważyłeś **\*.dll? To kopiowanie wszystkich .dllplików we wszystkich katalogach. Możesz łatwo **\*.*skopiować całe drzewo katalogów.
kjbartel
30

Niedawno miałem ten sam problem, gdy próbowałem zbudować pakiet NuGet EmguCV, w tym zarówno zarządzane zestawy, jak i niezarządzane udostępnione biblioteki (które również musiały być umieszczone w x86podkatalogu), które musiały być automatycznie kopiowane do katalogu wyjściowego kompilacji po każdej kompilacji .

Oto rozwiązanie, które wymyśliłem, które opiera się tylko na NuGet i MSBuild:

  1. Umieść zarządzane zespoły w /lib katalogu pakietu (część oczywista), a niezarządzane biblioteki współużytkowane i powiązane pliki (np. Pakiety pdb) w /buildpodkatalogu (zgodnie z opisem w dokumentacji NuGet ).

  2. Zmień nazwę wszystkich niezarządzanych *.dll zakończeń plików na coś innego, na przykład *.dl_aby zapobiec narzekaniu przez program NuGet na temat umieszczania domniemanych zestawów w niewłaściwym miejscu ( „Problem: zestaw poza folderem lib” ).

  3. Dodaj niestandardowy <PackageName>.targets plik do /buildpodkatalogu z podobną zawartością (patrz opis poniżej):

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

Powyższy .targetsplik zostanie wstrzyknięty podczas instalacji pakietu NuGet w docelowym pliku projektu i jest odpowiedzialny za kopiowanie natywnych bibliotek do katalogu wyjściowego.

  • <AvailableItemName Include="NativeBinary" /> dodaje nowy element „Akcja kompilacji” dla projektu (który jest również dostępny na liście rozwijanej „Akcja kompilacji” w programie Visual Studio).

  • <NativeBinary Include="... dodaje biblioteki natywne umieszczone w /build/x86 bieżącym projekcie i udostępnia je celowi niestandardowemu, który kopiuje te pliki do katalogu wyjściowego.

  • <TargetPath>x86</TargetPath> dodaje niestandardowe metadane do plików i informuje cel niestandardowy, aby skopiować pliki natywne do pliku x86 podkatalogu rzeczywistego katalogu wyjściowego.

  • Plik <PrepareForRunDependsOn ...Blok dodaje cel niestandardowy do listy celów build zależy, zobacz Microsoft.Common.targets plik o szczegóły.

  • Cel niestandardowy,, CopyNativeBinarieszawiera dwa zadania kopiowania. Pierwszy z nich odpowiada za kopiowanie dowolnych *.dl_plików do katalogu wyjściowego podczas zmiany ich rozszerzenia z powrotem na oryginalne *.dll. Drugi po prostu kopiuje resztę (na przykład dowolne *.pdbpliki) do tej samej lokalizacji. Można to zastąpić pojedynczym zadaniem kopiowania i skryptem install.ps1, który musiał zmienić nazwy wszystkich *.dl_plików na*.dll podczas instalacji pakietu.

Jednak to rozwiązanie nadal nie skopiowałoby natywnych plików binarnych do katalogu wyjściowego innego projektu odwołującego się do tego, który początkowo zawiera pakiet NuGet. Nadal musisz odwoływać się do pakietu NuGet w swoim „ostatecznym” projekcie.

buygrush
źródło
4
Jednak to rozwiązanie nadal nie skopiowałoby natywnych plików binarnych do katalogu wyjściowego innego projektu odwołującego się do tego, który początkowo zawiera pakiet NuGet. Nadal musisz odwoływać się do pakietu NuGet również w swoim„ ostatecznym ”projekcie. To jest pokaż mi korek. Zwykle oznacza to, że musisz dodać pakiet NuGet do wielu projektów (takich jak testy jednostkowe), w przeciwnym razie zostanie wyświetlony DllNotFoundException.
kjbartel
2
trochę drastyczne, aby zmienić nazwy plików itp. tylko z powodu ostrzeżenia.
możesz usunąć ostrzeżenie, dodając <NoWarn>NU5100</NoWarn>do pliku projektu
Florian Koch
28

Oto alternatywa, która używa .targetsdo wstrzyknięcia natywnej biblioteki DLL w projekcie z następującymi właściwościami.

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

Główną zaletą tej techniki jest to, że natywna biblioteka DLL jest przejściowo kopiowana do bin/folderu projektów zależnych .

Zobacz układ .nuspecpliku:

Zrzut ekranu Eksploratora pakietów NuGet

Oto .targetsplik:

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

Spowoduje to wstawienie MyNativeLib.dllpliku tak, jakby był częścią oryginalnego projektu (ale co ciekawe, plik nie jest widoczny w programie Visual Studio).

Zwróć uwagę na <Link>element, który ustawia nazwę pliku docelowego w bin/folderze.

Benoit Blanchon
źródło
Działa świetnie z niektórymi plikami .bat i .ps1, które muszę dołączyć jako część mojej usługi Azure - dzięki :)
Zhaph - Ben Duguid
„(Ale co ciekawe, plik nie jest widoczny w programie Visual Studio)”. - pliki projektów są analizowane przez sam VS AFAIK, więc elementy dodane w zewnętrznych plikach .target (lub te utworzone dynamicznie w wykonaniu docelowym) nie są wyświetlane.
kkm
Czym różni się to od poprzedniej odpowiedzi innej niż zmiana z Contentna None?
kjbartel
3
wow, jesteś szybki. w każdym razie, jeśli zdecydujesz się to zrobić, możesz przynajmniej zapytać „czym różni się to od mojej odpowiedzi”. że imo byłoby bardziej sprawiedliwe niż edytowanie oryginalnego pytania, udzielenie na nie odpowiedzi samodzielnie, a następnie promowanie swojej odpowiedzi w komentarzach innych osób. nie wspominając o tym, że osobiście podoba mi się ta konkretna odpowiedź bardziej niż twoja - jest zwięzła, na temat i łatwiejsza do odczytania
Maksim Satsikau
3
@MaksimSatsikau Możesz spojrzeć na historię. Zredagowałem pytanie, aby było jaśniejsze, po czym odpowiedziałem na pytanie. Ta odpowiedź nadeszła kilka tygodni później i była właściwie kopią. Przepraszam, jeśli uznałem to za niegrzeczne.
kjbartel
19

Jeśli ktoś się na to natknie.

Nazwa .targetspliku MUSI być równa identyfikatorowi pakietu NuGet

Cokolwiek innego nie zadziała.

Kredyty idą do: https://sushihangover.github.io/nuget-and-msbuild-targets/

Powinienem był przeczytać dokładniej, ponieważ zostało to tutaj odnotowane. Zajęło mi to wieki ...

Dodaj niestandardowy <PackageName>.targets

DirtyLittleHelper
źródło
3
oszczędzasz cały mój dzień!
zheng yu
1
Rozwiązałeś trwający tydzień problem z czymś innym. Dzięki tobie i tej stronie github.
Glenn Watson
13

Jest trochę za późno, ale specjalnie do tego stworzyłem pakiet nuget.

Pomysł polega na tym, aby mieć dodatkowy specjalny folder w pakiecie nuget. Jestem pewien, że znasz już Lib i Content. Utworzony przeze mnie pakiet NuGet szuka folderu o nazwie Wyjście i skopiuje wszystko, co się tam znajduje, do folderu wyjściowego projektów.

Jedyne, co musisz zrobić, to dodać zależność NuGet do pakietu http://www.nuget.org/packages/Baseclass.Contrib.Nuget.Output/

Napisałem o tym post na blogu: http://www.baseclass.ch/blog/Lists/Beitraege/Post.aspx?ID=6&mobile=0

Daniel Romero
źródło
To cudownie! Jednak działa to tylko w bieżącym projekcie. Jeśli projekt jest „biblioteką klas” i chcesz dodać jako zależność do „aplikacji sieciowej” na przykład, biblioteki DLL nie zostaną zbudowane w aplikacji internetowej! Moja „szybka poprawka” to: utwórz NuGet dla swojej biblioteki i zastosuj go do biblioteki klas oraz utwórz inny Nuget dla zależności (w tym przypadku biblioteki DLL) i zastosuj do aplikacji WebApplication. Jakieś najlepsze rozwiązanie?
Wagner Leonardi
Wygląda na to, że ten projekt został utworzony tylko dla platformy .NET 4.0 (Windows). Czy planujesz zaktualizować go, aby obsługiwał również przenośne biblioteki klas?
Ani
1

Istnieje czyste rozwiązanie C #, które wydaje mi się dość łatwe w użyciu i nie muszę przejmować się ograniczeniami NuGet. Wykonaj następujące kroki:

Uwzględnij bibliotekę natywną w projekcie i ustaw jej właściwość Build Action na Embedded Resource.

Wklej następujący kod do klasy, w której PInvoke tej biblioteki.

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

Wywołaj tę metodę z konstruktora statycznego, jak poniżej, UnpackNativeLibrary("win32");a rozpakuje bibliotekę na dysk tuż przed jej użyciem. Oczywiście musisz mieć pewność, że masz uprawnienia do zapisu w tej części dysku.

Ondrej Janacek
źródło
1

To jest stare pytanie, ale mam teraz ten sam problem i znalazłem rozwiązanie, które jest trochę trudne, ale bardzo proste i skuteczne: utwórz w folderze zawartości standardowej Nuget następującą strukturę z jednym podfolderem dla każdej konfiguracji:

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

Po spakowaniu pliku nuspec zostanie wyświetlony następujący komunikat dla każdej biblioteki natywnej w folderach Debug i Release:

Problem: zestaw poza folderem lib. Opis: zestaw „Content \ Bin \ Debug \ ??????. Dll” nie znajduje się w folderze „lib”, a zatem nie zostanie dodany jako odniesienie, gdy pakiet jest instalowany w projekcie. Rozwiązanie: Przenieś go do folderu „lib”, jeśli należy się do niego odwoływać.

Nie potrzebujemy takiego „rozwiązania”, ponieważ jest to tylko nasz cel: aby biblioteki natywne nie były dodawane jako odwołania do zestawów NET.

Zalety to:

  1. Proste rozwiązanie bez kłopotliwych skryptów z dziwnymi efektami, które są trudne do zresetowania podczas odinstalowywania pakietu.
  2. Nuget zarządza bibliotekami natywnymi jak każdą inną zawartością podczas instalowania i odinstalowywania.

Wady to:

  1. Potrzebujesz folderu dla każdej konfiguracji (ale zwykle są tylko dwa: Debugowanie i Wydanie, a jeśli masz inną zawartość, która musi być zainstalowana w każdym folderze konfiguracyjnym, jest to albo droga)
  2. Biblioteki natywne muszą być zduplikowane w każdym folderze konfiguracyjnym (ale jeśli masz różne wersje bibliotek natywnych dla każdej konfiguracji, jest to albo droga)
  3. Ostrzeżenia dla każdej natywnej biblioteki DLL w każdym folderze (ale jak powiedziałem, ostrzeżenia są wysyłane do twórcy pakietu w czasie pakowania, a nie do użytkownika pakietu w czasie instalacji VS)
SERWare
źródło
0

Nie mogę rozwiązać konkretnego problemu, ale mogę zasugerować.

Twój kluczowy wymóg to: „I nie rejestruj automatycznie referencji” .....

Musisz więc zapoznać się z „elementami rozwiązań”

Zobacz odniesienie tutaj:

Dodawanie elementów na poziomie rozwiązania w pakiecie NuGet

Będziesz musiał napisać kilka voodoo w PowerShell, aby pobrać kopię swojej natywnej biblioteki dll do jej domu (ponownie, ponieważ NIE chcesz, aby voodoo automatycznie dodawało referencje)

Oto plik ps1, który napisałem ..... aby umieścić pliki w folderze referencyjnym innej firmy.

Jest tam wystarczająco dużo miejsca, aby dowiedzieć się, jak skopiować natywną bibliotekę dll do jakiegoś „domu” ... bez konieczności rozpoczynania od zera.

Ponownie, nie jest to trafienie bezpośrednie, ale lepsze niż nic.

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
źródło
-2

Umieść to w folderze zawartości

polecenie nuget pack [projfile].csprojzrobi to automatycznie, jeśli oznaczysz pliki jako zawartość.

następnie edytuj plik projektu, jak wspomniano tutaj, dodając element ItemGroup & NativeLibs & None

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

pracował dla mnie

Sharon Salmon
źródło