Adicione arquivos nativos do pacote NuGet ao diretório de saída do projeto

126

Estou tentando criar o pacote NuGet para um assembly .Net que faz pinvoke para uma dll win32 nativa. Preciso compactar o assembly e a dll nativa com o assembly adicionado às referências do projeto (sem problemas nesta parte) e a dll nativa deve ser copiada no diretório de saída do projeto ou em algum outro diretório relativo.

Minhas perguntas são:

  1. Como empacotar a dll nativa sem o visual studio tentar adicioná-lo à lista de referências?
  2. Preciso escrever um install.ps1 para copiar a dll nativa? Se sim, como posso acessar o conteúdo do pacote para copiá-lo?
AlonFStackoverflow
fonte
1
Há suporte para bibliotecas específicas de tempo de execução / arquitetura, mas a documentação do recurso está ausente e parece ser específica da UWP. docs.microsoft.com/en-us/nuget/create-packages/…
Wouter

Respostas:

131

O uso do Copydestino no arquivo de destinos para copiar as bibliotecas necessárias não copiará esses arquivos para outros projetos que fazem referência ao projeto, resultando em um arquivo DllNotFoundException. No entanto, isso pode ser feito com um arquivo de destinos muito mais simples, usando um Noneelemento, pois o MSBuild copiará todos os Nonearquivos para projetos de referência.

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

Inclua o arquivo de destinos no builddiretório do pacote nuget, juntamente com as bibliotecas nativas necessárias. O arquivo de destinos incluirá todos os dllarquivos em todos os diretórios filhos do builddiretório. Portanto, para adicionar uma versão x86e x64uma biblioteca nativa usada por um Any CPUassembly gerenciado, você terminaria com uma estrutura de diretório semelhante à seguinte:

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

Os mesmos diretórios x86e x64serão criados no diretório de saída do projeto quando criados. Se você não precisa de subdiretórios em seguida, o **e %(RecursiveDir)podem ser removidos e, em vez incluir os arquivos necessários no builddiretório diretamente. Outros arquivos de conteúdo necessários também podem ser adicionados da mesma maneira.

Os arquivos adicionados como Noneno arquivo de destinos não serão mostrados no projeto quando abertos no Visual Studio. Se você está se perguntando por que não uso a Contentpasta no nupkg, é porque não há como definir o CopyToOutputDirectoryelemento sem usar um script PowerShell (que só será executado no Visual Studio, não no prompt de comando, nos servidores de compilação ou no outros IDEs, e não é suportado nos projetos project.json / xproj DNX ) e eu prefiro usar Linka nos arquivos em vez de ter uma cópia adicional dos arquivos no projeto.

Atualização: Embora isso também deva funcionar, em Contentvez de Noneparecer que existe um bug no msbuild, os arquivos não serão copiados para referenciar projetos mais de uma etapa removida (por exemplo, proj1 -> proj2 -> proj3, proj3 não obterá os arquivos do pacote NuGet do proj1, mas o proj2 será).

kjbartel
fonte
4
Senhor, você é um gênio! Funciona como um encanto. Obrigado.
MoonStom
Querendo saber por que a condição é '$(MSBuildThisFileDirectory)' != '' And HasTrailingSlash('$(MSBuildThisFileDirectory)')necessária? Eu pensei que o MSBuildThisFileDirectoryestá sempre definido. Quando não seria esse o caso?
kkm
@kkm Honestamente. Eu não acho que é necessário. Eu nem me lembro de onde eu o peguei originalmente.
Kjbartel # 23/15
@kkm Originalmente modifiquei o pacote nuget System.Data.SQLite e parece que deixei isso para trás quando removi todas as outras porcarias que eles incluíam. Arquivo de destinos original .
Kjbartel # 23/15
2
@SuperJMN Há curingas lá. Você não percebeu o **\*.dll? Isso é copiar todos os .dllarquivos em todos os diretórios. Você poderia fazer isso facilmente **\*.*para copiar uma árvore de diretórios inteira.
kjbartel
30

Recentemente, tive o mesmo problema ao tentar criar um pacote EmguCV NuGet, incluindo assemblies gerenciados e lirários compartilhados não gerenciados (que também precisavam ser colocados em um x86subdiretório), que precisavam ser copiados automaticamente para o diretório de saída da compilação após cada compilação .

Aqui está uma solução que eu criei, que depende apenas do NuGet e MSBuild:

  1. Coloque os assemblies gerenciados no /libdiretório do pacote (parte óbvia) e as bibliotecas compartilhadas não gerenciadas e os arquivos relacionados (por exemplo, pacotes .pdb) no /buildsubdiretório (conforme descrito nos documentos do NuGet ).

  2. Renomeie todas as *.dllterminações de arquivos não gerenciados para algo diferente, por exemplo, *.dl_para impedir que o NuGet gagueje sobre as alegadas assembléias serem colocadas em um local errado ( "Problema: montagem fora da pasta lib" ).

  3. Adicione um <PackageName>.targetsarquivo personalizado no /buildsubdiretório com algo como o seguinte conteúdo (veja abaixo uma descrição):

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

O .targetsarquivo acima será injetado em uma instalação do pacote NuGet no arquivo de projeto de destino e é responsável por copiar as bibliotecas nativas para o diretório de saída.

  • <AvailableItemName Include="NativeBinary" /> adiciona um novo item "Build Action" para o projeto (que também fica disponível no menu suspenso "Build Action", dentro do Visual Studio).

  • <NativeBinary Include="...adiciona as bibliotecas nativas inseridas no /build/x86projeto atual e as torna acessíveis ao destino personalizado que copia esses arquivos no diretório de saída.

  • <TargetPath>x86</TargetPath>adiciona metadados personalizados aos arquivos e informa ao destino personalizado para copiar os arquivos nativos no x86subdiretório do diretório de saída real.

  • O <PrepareForRunDependsOn ...bloco adiciona o destino personalizado à lista de destinos da qual a compilação depende, consulte o arquivo Microsoft.Common.targets para obter detalhes.

  • O destino personalizado,, CopyNativeBinariescontém duas tarefas de cópia. O primeiro é responsável por copiar todos os *.dl_arquivos para o diretório de saída enquanto altera sua extensão para o original *.dll. O segundo simplesmente copia o restante (por exemplo, qualquer *.pdbarquivo) para o mesmo local. Isso poderia ser substituído por uma tarefa de cópia única e um script install.ps1 que precisou renomear todos os *.dl_arquivos *.dlldurante a instalação do pacote.

No entanto, essa solução ainda não copiava os binários nativos no diretório de saída de outro projeto que referencia o que inicialmente inclui o pacote NuGet. Você ainda precisa fazer referência ao pacote NuGet no seu projeto "final" também.

buygrush
fonte
4
" No entanto, essa solução ainda não copiava os binários nativos no diretório de saída de outro projeto que referencia o que inicialmente inclui o pacote NuGet. Você ainda precisa fazer referência ao pacote NuGet no seu projeto" final "também. " mostre rolha para mim. Isso geralmente significa que você precisa adicionar o pacote nuget a vários projetos (como testes de unidade), caso contrário você será DllNotFoundExceptionlançado.
Kjbartel #
2
um pouco drástico para renomear os arquivos etc apenas por causa do aviso.
você pode remover o aviso adicionando <NoWarn>NU5100</NoWarn>ao seu arquivo de projeto
Florian Koch
28

Aqui está uma alternativa que usa o .targetspara injetar a DLL nativa no projeto com as seguintes propriedades.

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

O principal benefício dessa técnica é que a DLL nativa é copiada transitivamente para a bin/pasta de projetos dependentes .

Veja o layout do .nuspecarquivo:

Captura de tela do NuGet Package Explorer

Aqui está o .targetsarquivo:

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

Isso insere MyNativeLib.dllcomo se fosse parte do projeto original (mas, curiosamente, o arquivo não é visível no Visual Studio).

Observe o <Link>elemento que define o nome do arquivo de destino na bin/pasta.

Benoit Blanchon
fonte
Funciona um tratamento com algum .bat e .ps1 arquivos eu preciso incluir como parte do meu serviço Azure - obrigado :)
Zhaph - Ben Duguid
"(Mas, curiosamente, o arquivo não é visível no Visual Studio)." - os arquivos do projeto são analisados ​​pelo próprio VS AFAIK, portanto, os itens adicionados nos arquivos .target externos (ou aqueles criados dinamicamente na execução do destino) não são mostrados.
kkm
Como isso é diferente da outra resposta anterior, além de mudar de Contentpara None?
kjbartel
3
Uau, você é rápido. de qualquer forma, se você optar por fazê-lo, poderá pelo menos perguntar 'como isso é diferente da minha resposta'. esse imo seria mais justo do que editar a pergunta original, responder você mesmo e promover sua resposta nos comentários de outras pessoas. para não mencionar que eu, pessoalmente, como esta resposta particular, melhor que a sua - é conciso, direto ao ponto e fácil de ler
Maksim Satsikau
3
@MaksimSatsikau Você pode querer olhar para a história. Editei a pergunta para torná-la mais clara e respondi à pergunta. Esta resposta veio algumas semanas depois e era efetivamente uma cópia. Desculpe se achei isso rude.
kjbartel
19

Se mais alguém se deparar com isso.

O .targetsnome do arquivo DEVE ser igual ao ID do pacote NuGet

Qualquer outra coisa não vai funcionar.

Os créditos vão para: https://sushihangover.github.io/nuget-and-msbuild-targets/

Eu deveria ter lido mais detalhadamente, como é realmente observado aqui. Levou-me idades ..

Adicionar um costume <PackageName>.targets

DirtyLittleHelper
fonte
3
você salva meu dia inteiro!
precisa
1
Você corrigiu um problema de uma semana com outra coisa. Obrigado a você e à página do github.
Glenn Watson
13

É um pouco tarde, mas eu criei um pacote de nuget exatamente para isso.

A idéia é ter uma pasta especial adicional em seu pacote de nuget. Tenho certeza que você já conhece a Lib e o Content. O pacote de pepitas que criei procura uma pasta chamada Saída e copia tudo o que está lá na pasta de saída dos projetos.

A única coisa que você precisa fazer é adicionar uma dependência de nuget ao pacote http://www.nuget.org/packages/Baseclass.Contrib.Nuget.Output/

Eu escrevi um post sobre isso: http://www.baseclass.ch/blog/Lists/Beitraege/Post.aspx?ID=6&mobile=0

Daniel Romero
fonte
Fantástico! No entanto, isso funciona apenas no projeto atual. Se o projeto for uma "Biblioteca de Classes" e você desejar adicionar como dependência a um "Aplicativo da Web", por exemplo, as DLLs não serão criadas no aplicativo da Web! Minha "solução rápida" é: crie um NuGet para sua biblioteca e aplique à Class Library, e crie outro Nuget para dependências (DLLs neste caso) e aplique-o ao WebApplication. Alguma melhor solução para isso?
Wagner Leonardi
Parece que você criou este projeto apenas para o .NET 4.0 (Windows). Você planeja atualizá-lo para dar suporte a bibliotecas de classes portáteis também?
Ani
1

Existe uma solução C # pura que eu acho bastante fácil de usar e não preciso me preocupar com as limitações do NuGet. Siga esses passos:

Inclua a biblioteca nativa no seu projeto e defina sua propriedade Build Action como Embedded Resource.

Cole o seguinte código na classe em que você invocou 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());
    }
}

Chame esse método do construtor estático da seguinte maneira UnpackNativeLibrary("win32");e ele descompactará a biblioteca no disco antes de você precisar. Obviamente, você precisa ter certeza de que possui permissões de gravação para essa parte do disco.

Ondrej Janacek
fonte
1

Esta é uma pergunta antiga, mas agora tenho o mesmo problema e encontrei uma resposta um pouco complicada, mas muito simples e eficaz: crie na pasta Nuget Standard Content a seguinte estrutura com uma subpasta para cada configuração:

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

Ao compactar o arquivo nuspec, você receberá a seguinte mensagem para cada biblioteca nativa nas pastas Debug and Release:

Problema: Montagem fora da pasta lib. Descrição: o assembly 'Content \ Bin \ Debug \ ??????. Dll' não está dentro da pasta 'lib' e, portanto, não será adicionado como referência quando o pacote for instalado em um projeto. Solução: Mova-o para a pasta 'lib', se for necessário fazer referência.

Não precisamos dessa "solução" porque esse é apenas o nosso objetivo: que as bibliotecas nativas não sejam adicionadas como referências ao NET Assemblies.

As vantagens são:

  1. Solução simples, sem scripts pesados, com efeitos estranhos, difíceis de redefinir na desinstalação de pacotes.
  2. O Nuget gerencia as bibliotecas nativas como qualquer outro conteúdo ao instalar e desinstalar.

As desvantagens são:

  1. Você precisa de uma pasta para cada configuração (mas geralmente existem apenas duas: Debug e Release, e se você tiver outro conteúdo que deve ser instalado em cada pasta de configuração, este é o caminho a seguir)
  2. Bibliotecas nativas devem ser duplicadas em cada pasta de configuração (mas se você tiver versões diferentes das bibliotecas nativas para cada configuração, este é o caminho a seguir)
  3. Os avisos para cada dll nativa em cada pasta (mas como eu disse, eles são emitidos para o criador do pacote no momento do pacote, não para o usuário do pacote no momento da instalação do VS)
SERWare
fonte
0

Não consigo resolver o seu problema exato, mas posso lhe dar uma sugestão.

Seu principal requisito é: "E não registre automaticamente a referência" .....

Então você terá que se familiarizar com os "itens da solução"

Veja a referência aqui:

Adicionando itens no nível da solução em um pacote NuGet

Você precisará escrever um vodu do powershell para obter a cópia da sua dll nativa em sua casa (novamente, porque você NÃO deseja que o vodu de adição automática de referência seja acionado)

Aqui está um arquivo ps1 que escrevi ... para colocar arquivos em uma pasta de referências de terceiros.

Há o suficiente para você descobrir como copiar sua dll nativa para alguma "casa" ... sem ter que começar do zero.

Novamente, não é um golpe direto, mas é melhor 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
fonte
-2

Coloque é a pasta de conteúdo

o comando nuget pack [projfile].csprojfará isso por você automaticamente se você marcar os arquivos como conteúdo.

edite o arquivo do projeto conforme mencionado aqui, adicionando o elemento ItemGroup & NativeLibs & None

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

trabalhou para mim

Sharon Salmon
fonte