Partilhar via


Solucionar problemas de referências de assembly

Uma das tarefas mais importantes no MSBuild e no processo de compilação do .NET é resolver referências de assembly, o que acontece na ResolveAssemblyReference tarefa. Este artigo explica alguns detalhes de como ResolveAssemblyReference funciona e como resolver falhas de compilação que podem ocorrer quando ResolveAssemblyReference não é possível resolver uma referência. Para investigar falhas de referência de assembly, poderá querer instalar o Visualizador de Logs Estruturados para ver os registos do MSBuild. As capturas de ecrã deste artigo são retiradas do Visualizador de Registos Estruturados.

O objetivo de ResolveAssemblyReference é capturar todas as referências especificadas em ficheiros .csproj (ou noutro local) através do item <Reference> e mapeá-las para caminhos de ficheiros de assemblagem no sistema de ficheiros.

Os compiladores só podem aceitar um .dll caminho do sistema de ficheiros como referência, por isso ResolveAssemblyReference converte cadeias, como mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, que aparecem nos ficheiros do projeto em caminhos como C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.dll, que depois são enviados para o compilador através do /r switch.

Além disso, ResolveAssemblyReference determina o conjunto completo (na verdade, o fecho transitivo em termos de teoria dos grafos) de todas as .dll e .exe referências recursivamente, e para cada uma delas determina se deve ser copiada para o diretório de saída da compilação ou não. Não faz a cópia propriamente dita (que é tratada mais tarde, após a fase de compilação), mas prepara uma lista de ficheiros para copiar.

ResolveAssemblyReference é invocado a partir do alvo ResolveAssemblyReferences.

Captura de ecrã do visualizador de registos a mostrar quando o ResolveAssemblyReferences é chamado no processo de compilação.

Se reparares na ordenação, ResolveAssemblyReferences acontece antes de Compile, e claro, CopyFilesToOutputDirectory acontece depois de Compile.

Observação

ResolveAssemblyReferencea tarefa é invocada no ficheiro padrão .targets nas pastas de instalação do MSBuildMicrosoft.Common.CurrentVersion.targets. Também pode navegar online pelos alvos do MSBuild do SDK .NET em https://github.com/dotnet/msbuild/blob/a936b97e30679dcea4d99c362efa6f732c9d3587/src/Tasks/Microsoft.Common.CurrentVersion.targets#L1991-L2140. Este link mostra exatamente onde a ResolveAssemblyReference tarefa é invocada no .targets ficheiro.

Entradas ResolveAssemblyReference

ResolveAssemblyReference é abrangente quanto ao registo das suas entradas:

Captura de ecrã a mostrar parâmetros de entrada para a tarefa ResolveAssemblyReference.

O Parameters nó é padrão para todas as tarefas, mas adicionalmente ResolveAssemblyReference regista o seu próprio conjunto de informação em Inputs (que é basicamente igual ao em Parameters, mas estruturado de forma diferente).

Os inputs mais importantes são Assemblies e AssemblyFiles:

    <ResolveAssemblyReference
        Assemblies="@(Reference)"
        AssemblyFiles="@(_ResolvedProjectReferencePaths);@(_ExplicitReference)"

Assemblies utiliza o conteúdo do Reference item MSBuild no momento em que ResolveAssemblyReference é invocado para o projeto. Todos os metadados e referências de assembly, incluindo as suas referências NuGet, devem estar contidos neste item. Cada referência tem um conjunto rico de metadados associados:

Captura de ecrã que mostra metadados numa referência de assembly.

AssemblyFiles provém do ResolveProjectReference item de saída do alvo chamado _ResolvedProjectReferencePaths. ResolveProjectReference executa antes ResolveAssemblyReference e converte <ProjectReference> itens em caminhos de assemblies construídos no disco. Assim, o AssemblyFiles irá conter os conjuntos construídos por todos os projetos referenciados do projeto atual:

Captura de ecrã que mostra AssemblyFiles.

Outra entrada útil é o parâmetro booleano FindDependencies , que retira o seu valor da _FindDependencies propriedade:

FindDependencies="$(_FindDependencies)"

Pode definir esta propriedade na false sua build para desligar a análise de assemblies de dependências transitivas.

Algoritmo "ResolveAssemblyReference"

O algoritmo simplificado para a ResolveAssemblyReference tarefa é o seguinte:

  1. Registo de entradas.
  2. Verifica a MSBUILDLOGVERBOSERARSEARCHRESULTS variável de ambiente. Defina esta variável para qualquer valor para obter registos mais detalhados.
  3. Inicialize o objeto da tabela de referências.
  4. Leia o ficheiro de cache a partir do obj diretório (se estiver presente).
  5. Calcule o encerramento das dependências.
  6. Constrói as tabelas de saída.
  7. Escreve o ficheiro de cache no obj diretório.
  8. Regista os resultados.

O algoritmo recolhe a lista de entrada de assemblies (tanto de metadados como de referências de projeto), recupera a lista de referências de cada assembly que processa (lendo metadados) e constrói um conjunto completo (fecho transitivo) de todos os assemblies referenciados, resolvendo-os a partir de vários locais (incluindo o GAC, AssemblyFoldersEx, entre outros).

As assembleias referenciadas são adicionadas iterativamente à lista até que não sejam adicionadas mais novas referências. Depois o algoritmo pára.

As referências diretas que forneceu para a tarefa são chamadas de referências primárias. Montagens indiretas que foram adicionadas ao conjunto devido a uma referência transitiva são chamadas de Dependências. O registo de cada assemblagem indireta mantém o registo de todos os itens primários ("raiz") que levaram à sua inclusão e dos respetivos metadados.

Resultados da tarefa ResolveAssemblyReference

ResolveAssemblyReference fornece registo detalhado dos resultados:

Captura de ecrã que mostra os resultados do ResolveAssemblyReference no visualizador de registos estruturados.

Os conjuntos de assemblies resolvidos dividem-se em duas categorias: referências primárias e dependências. As referências primárias eram especificadas explicitamente como referências ao projeto em construção. As dependências foram inferidas a partir de referências de referências de forma transitiva.

Importante

ResolveAssemblyReference lê os metadados da assembly para determinar as referências de uma determinada assembly. Quando o compilador C# emite uma assembleia, só adiciona referências a assemblies que são realmente necessárias. Assim, pode acontecer que, ao compilar um determinado projeto, este especifique uma referência desnecessária que não será incorporada na assembly. É aceitável adicionar referências ao projeto que não são necessárias; São ignorados.

Metadados do CopyLocal item

As referências também podem ter os CopyLocal metadados ou não. Se a referência tiver CopyLocal = true, ela será depois copiada para o diretório de saída pelo alvo CopyFilesToOutputDirectory. Neste exemplo, DataFlow definiu CopyLocal como verdadeiro, enquanto Immutable não:

Captura de ecrã a mostrar as definições do CopyLocal para algumas referências.

Se os CopyLocal metadados estiverem completamente ausentes, assume-se que são verdadeiros por defeito. Por defeito, ResolveAssemblyReference tenta copiar dependências para a saída, a menos que encontre uma razão para não o fazer. ResolveAssemblyReference regista as razões pelas quais escolheu ou não uma determinada referência CopyLocal .

Todas as possíveis razões para CopyLocal a decisão estão enumeradas na tabela seguinte. É útil conhecer estas strings para poder procurá-las nos logs de compilação.

Copiar estado local Description
Undecided O estado da cópia local está atualmente por decidir.
YesBecauseOfHeuristic A referência devia ter CopyLocal='true' porque não havia razão para ser 'não'.
YesBecauseReferenceItemHadMetadata A Referência deve ter CopyLocal='true' porque o seu item de origem tem Privado='true'
NoBecauseFrameworkFile A referência deve ter CopyLocal='false' porque é um ficheiro de framework.
NoBecausePrerequisite A referência deve ter CopyLocal='false' porque é um ficheiro pré-requisito.
NoBecauseReferenceItemHadMetadata A referência deve ter CopyLocal='false' porque o atributo Private está definido como 'falso' no projeto.
NoBecauseReferenceResolvedFromGAC A referência deve ter CopyLocal='false' porque foi resolvida pelo GAC.
NoBecauseReferenceFoundInGAC Comportamento legado, CopyLocal='false' quando o componente é encontrado no GAC (mesmo quando foi resolvido noutro local).
NoBecauseConflictVictim A referência deve conter CopyLocal='false' porque perdeu um conflito com um ficheiro de assembly do mesmo nome.
NoBecauseUnresolved A referência não foi resolvida. Não pode ser copiado para o diretório bin porque não foi encontrado.
NoBecauseEmbedded A referência foi incorporada. Não deve ser copiado para a pasta bin porque não será carregado em tempo de execução.
NoBecauseParentReferencesFoundInGAC A propriedade copyLocalDependenciesWhenParentReferenceInGac está definida como falsa e todos os itens de origem originais foram encontrados no GAC.
NoBecauseBadImage O ficheiro de montagem fornecido não deve ser copiado porque é uma imagem má, possivelmente não gerida, possivelmente nem sequer uma assembleia.

Metadados privados de itens

Uma parte importante da determinação CopyLocal são os Private metadados de todas as referências primárias. Cada referência (primária ou dependente) tem uma lista de todas as referências primárias (itens de origem) que contribuíram para que essa referência fosse adicionada ao fecho.

  • Se nenhum dos itens de origem especificar Private metadados, CopyLocal é definido como True (ou não definido, que por defeito é True)
  • Se algum dos itens fonte especificar Private=true, CopyLocal é definido como True
  • Se nenhuma das assemblagens de origem especificar Private=true e pelo menos uma especificar Private=false, CopyLocal é definido como False

Qual referência configurou Privado para falso?

O último ponto é uma razão frequentemente usada para CopyLocal estar definido como falso: This reference is not "CopyLocal" because at least one source item had "Private" set to "false" and no source items had "Private" set to "true".

O MSBuild não nos diz qual referência está definida Private como falsa, mas o visualizador de registos estruturados adiciona Private metadados aos itens que tinham especificado acima:

Captura de ecrã que mostra o Private definido como falso no visualizador de registos estruturado.

Isto simplifica as investigações e indica-lhe exatamente qual referência causou a definição da dependência em questão com CopyLocal=false.

Cache de Assembleia Global

O Global Assembly Cache (GAC) desempenha um papel importante ao determinar se deve copiar referências para o output. Isto é lamentável porque o conteúdo do GAC é específico de cada máquina, o que resulta em problemas para compilações reproduzíveis (onde o comportamento difere consoante a máquina dependendo do estado da máquina, como o GAC).

Foram feitas correções recentes no ResolveAssemblyReference para aliviar a situação. Pode controlar o comportamento com estas duas novas entradas para ResolveAssemblyReference:

    CopyLocalDependenciesWhenParentReferenceInGac="$(CopyLocalDependenciesWhenParentReferenceInGac)"
    DoNotCopyLocalIfInGac="$(DoNotCopyLocalIfInGac)"

AssemblySearchPaths

Existem duas formas de personalizar a lista de caminhos que as pesquisas ResolveAssemblyReference utilizam ao tentar localizar um assembly. Para personalizar totalmente a lista, a propriedade AssemblySearchPaths pode ser definida antecipadamente. A ordem importa; se um conjunto estiver em dois locais, ResolveAssemblyReference interrompe a execução após encontrá-lo no primeiro local.

AssemblySearchPaths é uma lista delimitada por ponto e vírgula. Suporta um conjunto de marcadores incorporados (por exemplo, {HintPathFromItem} e {GAC}) que se expandem para locais reais durante a resolução.

Por defeito, projetos que não são ao estilo SDK usam a seguinte ordem de caminho de pesquisa:

  1. Ficheiros de montagem de candidatos ({CandidateAssemblyFiles})
  2. Propriedade ReferencePath ($(ReferencePath))
  3. Percursos de referência a partir de <Reference> itens ({HintPathFromItem})
  4. O diretório do framework de destino ({TargetFrameworkDirectory})
  5. Pastas de montagem a partir de AssemblyFolders.config ($(AssemblyFoldersConfigFileSearchPath))
  6. O registo ({Registry:...})
  7. Assemblies registadas herdadas ({AssemblyFolders})
  8. A Cache de Montagem Global (GAC) ({GAC})
  9. Trate o <Reference Include="..."> valor como um nome real de ficheiro ({RawFileName})
  10. O diretório de saída ($(OutDir))

O SDK .NET configura um padrão AssemblySearchPaths menor (excluindo, por defeito, as pesquisas no GAC/registo/diretório de saída):

  • {CandidateAssemblyFiles}
  • {HintPathFromItem}
  • {TargetFrameworkDirectory}
  • {RawFileName}

Para ver o valor efetivo da sua build, inspecione a SearchPaths entrada registada por ResolveAssemblyReference (por exemplo, no MSBuild Structured Log Viewer) ou processe previamente o projeto com msbuild /pp.

Cada entrada pode ser desativada definindo a flag relevante para false:

  • A pesquisa de ficheiros do projeto atual é desativada ao definir a AssemblySearchPath_UseCandidateAssemblyFiles propriedade como false.
  • A pesquisa na propriedade de caminho de referência (a partir de um .user ficheiro) é desativada ao definir a AssemblySearchPath_UseReferencePath propriedade como false.
  • O uso do caminho de dicas do item é desativado ao definir a AssemblySearchPath_UseHintPathFromItem propriedade como false.
  • Para desativar o uso do diretório com o runtime alvo do MSBuild, defina a propriedade AssemblySearchPath_UseTargetFrameworkDirectory como false.
  • Pesquisar diretórios de assembly a partir de AssemblyFolders.config é desativado ao definir a propriedade AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath como False.
  • Pesquisar no registo é desativado ao definir a AssemblySearchPath_UseRegistry propriedade para falso.
  • A pesquisa em pastas de assembly registadas antigas é desativada quando se define a propriedade AssemblySearchPath_UseAssemblyFolders para false.
  • Procurar no GAC é desativado quando a propriedade AssemblySearchPath_UseGAC é definida como false.
  • Tratar o Include da referência como um nome real de ficheiro é desativado ao definir a AssemblySearchPath_UseRawFileName propriedade como false.
  • A verificação da pasta de saída da aplicação é desativada ao definir a propriedade AssemblySearchPath_UseOutDir para false.

Houve um conflito

Uma situação comum é que o MSBuild emite um aviso sobre diferentes versões do mesmo conjunto serem usadas por diferentes referências. A solução envolve frequentemente adicionar um redirecionamento de ligação ao ficheiro app.config.

Uma forma útil de investigar estes conflitos é pesquisar no MSBuild Structured Log Viewer por "Houve um conflito". Mostra-lhe informações detalhadas sobre quais referências necessitam de quais versões do assembly em questão.