Solucionar problemas de referências de assembly
Uma das tarefas mais importantes no MSBuild e no processo de build do .NET é resolver referências de assembly, o que acontece na tarefa ResolveAssemblyReference
. Este artigo explica alguns dos detalhes de como ResolveAssemblyReference
funciona e como solucionar problemas de falhas de compilação que podem acontecer quando ResolveAssemblyReference
não é possível resolver uma referência. Para investigar falhas de referência de assembly, talvez você queira instalar o Visualizador de Logs Estruturados para exibir os logs do MSBuild. As capturas de tela neste artigo são tiradas do Visualizador de Logs Estruturados.
O objetivo de ResolveAssemblyReference
é agregar todas as referências especificadas em arquivos .csproj
(ou em outro lugar) por meio do item <Reference>
e mapeá-las para caminhos para arquivos de assembly no sistema de arquivos.
Os compiladores só podem aceitar um caminho .dll
no sistema de arquivos como referência, portanto, ResolveAssemblyReference
converte strings como mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
que aparecem em arquivos de projeto em caminhos como C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.dll
, que são então passados para o compilador por meio da opção /r
.
Além disso, ResolveAssemblyReference
determina o conjunto completo (na verdade, o fechamento transitivo em termos de teoria dos grafos) de todas as referências .dll
e .exe
recursivamente e, para cada um deles, determine se deve ser copiado para o diretório de saída de compilação ou não. Ele não faz a cópia real (que é tratada posteriormente, após a etapa de compilação real), mas prepara uma lista de itens de arquivos a serem copiados.
ResolveAssemblyReference
é invocado a partir do destino ResolveAssemblyReferences
:
Se você observar a ordenação, ResolveAssemblyReferences
está acontecendo antes de Compile
e, claro, CopyFilesToOutputDirectory
acontece depois de Compile
.
Observação
A tarefa ResolveAssemblyReference
é invocada no arquivo Microsoft.Common.CurrentVersion.targets
padrão .targets
nas pastas de instalação do MSBuild. Você também pode procurar os destinos do MSBuild do SDK do .NET online em https://github.com/dotnet/msbuild/blob/a936b97e30679dcea4d99c362efa6f732c9d3587/src/Tasks/Microsoft.Common.CurrentVersion.targets#L1991-L2140. Esse link mostra exatamente onde a tarefa ResolveAssemblyReference
é chamada no arquivo .targets
.
Entradas ResolveAssemblyReference
ResolveAssemblyReference
é abrangente sobre o registro de suas entradas:
O nó Parameters
é padrão para todas as tarefas, mas também ResolveAssemblyReference
registra seu próprio conjunto de informações em Entradas (que é basicamente o mesmo que em Parameters
, mas estruturado de forma diferente).
As entradas mais importantes são Assemblies
e AssemblyFiles
:
<ResolveAssemblyReference
Assemblies="@(Reference)"
AssemblyFiles="@(_ResolvedProjectReferencePaths);@(_ExplicitReference)"
Assemblies
usa o conteúdo do item MSBuild Reference
no momento em que ResolveAssemblyReference
é invocado para o projeto. Todos os metadados e referências de assembly, incluindo suas referências NuGet, devem estar contidos neste item. Cada referência tem um rico conjunto de metadados anexados a ela:
AssemblyFiles
vem do item de saída do destino ResolveProjectReference
chamado _ResolvedProjectReferencePaths
. ResolveProjectReference
é executado antes de ResolveAssemblyReference
e converte itens <ProjectReference>
em caminhos de assemblies criados no disco. Portanto, AssemblyFiles
conterá os assemblies construídos por todos os projetos referenciados do projeto atual:
Outra entrada útil é o parâmetro booleano FindDependencies
, que obtém seu valor da propriedade _FindDependencies
:
FindDependencies="$(_FindDependencies)"
Você pode definir essa propriedade como false
em sua compilação para desativar a análise de assemblies de dependência transitiva.
Algoritmo ResolveAssemblyReference
O algoritmo simplificado para a tarefa ResolveAssemblyReference
é a seguinte:
- Entradas de log.
- Verifique a variável de ambiente
MSBUILDLOGVERBOSERARSEARCHRESULTS
. Defina essa variável como qualquer valor para obter logs mais detalhados. - Inicialize a tabela de objeto de referências.
- Leia o arquivo de cache do diretório
obj
(se houver). - Calcule o fechamento de dependências.
- Crie as tabelas de saída.
- Grave o arquivo de cache no diretório
obj
. - Registre os resultados.
O algoritmo usa a lista de entrada de assemblies (de metadados e referências de projeto), recupera a lista de referências para cada assembly que processa (lendo metadados) e cria um conjunto completo (fechamento transitivo) de todos os assemblies referenciados e os resolve de vários locais (incluindo o GAC, AssemblyFoldersEx e assim por diante).
Os assemblies referenciados são adicionados à lista iterativamente até que não sejam adicionadas mais novas referências. Então o algoritmo para.
As referências diretas que você forneceu para a tarefa são chamadas de referências Primárias. Os assemblies indiretos que foram adicionados ao conjunto devido a uma referência transitiva são chamados de Dependência. O registro de cada assembly indireto controla todos os itens primários ("raiz") que levaram à sua inclusão e seus metadados correspondentes.
Resultados da tarefa ResolveAssemblyReference
ResolveAssemblyReference
fornece o registro detalhado dos resultados:
Os assemblies resolvidos são divididos em duas categorias: referências Primárias e Dependências. As referências primárias foram especificadas explicitamente como referências do projeto que está sendo construído. As dependências foram inferidas a partir de referências transitórias.
Importante
ResolveAssemblyReference
lê metadados de assembly para determinar as referências de um determinado assembly. Quando o compilador C# emite um assembly, ele adiciona apenas referências a assemblies que são realmente necessários. Portanto, pode acontecer que, quando você compilar um determinado projeto, o projeto especifique uma referência desnecessária que não será incorporada ao assembly. Não há problema em adicionar referências ao projeto que não são necessárias; eles são ignoradas.
Metadados de item CopyLocal
As referências também podem ter os metadados CopyLocal
ou não. Se a referência tiver CopyLocal = true
, ela será copiada posteriormente para o diretório de saída pelo destino CopyFilesToOutputDirectory
. Neste exemplo, DataFlow
tem CopyLocal
definido como true, enquanto Immutable
não:
Se os metadados CopyLocal
estiverem totalmente ausentes, eles serão considerados verdadeiros por padrão. Portanto, ResolveAssemblyReference
por padrão, tenta copiar dependências para a saída, a menos que encontre um motivo para não fazê-lo. ResolveAssemblyReference
registra as razões pelas quais escolheu uma referência específica como CopyLocal
ou não.
Todas as razões possíveis para a decisão de CopyLocal
são enumeradas na tabela a seguir. É útil conhecer essas strings para poder procurá-las nos logs de compilação.
Estado CopyLocal | Descrição |
---|---|
Undecided |
A cópia do estado local não está definida no momento. |
YesBecauseOfHeuristic |
A referência deveria ter CopyLocal='true' porque não era "não" por algum motivo. |
YesBecauseReferenceItemHadMetadata |
A referência deveria ter CopyLocal='true' porque seu item de origem tem Private='true' |
NoBecauseFrameworkFile |
A referência deveria ter CopyLocal='false' porque é um arquivo de estrutura. |
NoBecausePrerequisite |
A referência deveria ter CopyLocal='false' porque é um arquivo de pré-requisito. |
NoBecauseReferenceItemHadMetadata |
A referência deveria ter CopyLocal='false' porque o atributo Private está definido como 'false' no projeto. |
NoBecauseReferenceResolvedFromGAC |
A referência deveria ter CopyLocal='false' porque foi resolvida pelo GAC. |
NoBecauseReferenceFoundInGAC |
Comportamento herdado, CopyLocal='false' quando o assembly é encontrado no GAC (mesmo quando foi resolvido em outro lugar). |
NoBecauseConflictVictim |
A referência deveria ter CopyLocal='false' porque perdeu um conflito entre um arquivo de assembly com o mesmo nome. |
NoBecauseUnresolved |
A referência não foi resolvida. Ela não pode ser copiada para o diretório bin porque não foi encontrada. |
NoBecauseEmbedded |
A referência foi incorporada. Ela não deve ser copiada para o diretório bin porque não será carregada no runtime. |
NoBecauseParentReferencesFoundInGAC |
A propriedade copyLocalDependenciesWhenParentReferenceInGac é definida como false e todos os itens de origem pai foram encontrados no GAC. |
NoBecauseBadImage |
O arquivo de assembly fornecido não deve ser copiado porque é uma imagem ruim, possivelmente não gerenciada e não é um assembly. |
Metadados de itens privados
Uma parte importante de determinar CopyLocal
são os metadados Private
em todas as referências primárias. Cada referência (primária ou dependência) tem uma lista de todas as referências primárias (itens de origem) que contribuíram para que essa referência fosse adicionada ao fechamento.
- Se nenhum dos itens de origem especificar os metadados
Private
,CopyLocal
será definido comoTrue
(ou não definido, cujo padrão éTrue
) - Se qualquer um dos itens de origem especificar
Private=true
,CopyLocal
será definido comoTrue
- Se nenhum dos assemblies de origem especificar
Private=true
e pelo menos um especificarPrivate=false
,CopyLocal
será definido comoFalse
Qual referência definiu Private como false?
O último ponto é um motivo frequentemente usado para CopyLocal
ser definido como false: 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 informa qual referência definiu Private
como false, mas o visualizador de log estruturado adiciona metadados Private
aos itens especificados acima:
Isso simplifica as investigações e informa exatamente qual referência fez com que a dependência em questão fosse definida com CopyLocal=false
.
Cache de assemblies global
O Cache de Assemblies Global (GAC) desempenha um papel importante para determinar se as referências devem ser copiadas para a saída. Isso é lamentável porque o conteúdo do GAC é específico da máquina, resultando em problemas para compilações reproduzíveis (em que o comportamento difere em várias máquinas dependendo do estado dela, como o GAC).
Houve correções recentes feitas para ResolveAssemblyReference
para aliviar a situação. Você pode controlar o comportamento por essas duas novas entradas para ResolveAssemblyReference
:
CopyLocalDependenciesWhenParentReferenceInGac="$(CopyLocalDependenciesWhenParentReferenceInGac)"
DoNotCopyLocalIfInGac="$(DoNotCopyLocalIfInGac)"
AssemblySearchPaths
Há duas maneiras de personalizar a lista de pesquisas de caminhos ResolveAssemblyReference
na tentativa de localizar um assembly. Para personalizar totalmente a lista, a propriedade AssemblySearchPaths
pode ser definida com antecedência. A ordem é importante. Se um assembly estiver em dois locais, ResolveAssemblyReference
será interrompido depois de encontrá-lo no primeiro local.
Por padrão, há 10 pesquisas de ResolveAssemblyReference
locais (4 se você usar o SDK do .NET), e cada uma pode ser desabilitada definindo o sinalizador relevante como false:
- A pesquisa de arquivos do projeto atual é desativada definindo a propriedade
AssemblySearchPath_UseCandidateAssemblyFiles
como false. - A pesquisa da propriedade do caminho de referência (de um arquivo
.user
) é desativada definindo a propriedadeAssemblySearchPath_UseReferencePath
como false. - O uso do caminho de dica do item é desabilitado definindo a propriedade
AssemblySearchPath_UseHintPathFromItem
como false. - O uso do diretório com o runtime de destino do MSBuild é desabilitado definindo a propriedade
AssemblySearchPath_UseTargetFrameworkDirectory
como false. - A pesquisa de pastas de assembly de AssemblyFolders.config é desabilitada definindo a propriedade
AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath
como false. - A pesquisa no registro é desabilitada definindo a propriedade
AssemblySearchPath_UseRegistry
como false. - A pesquisa de pastas de assembly registradas herdadas é desabilitada definindo a propriedade
AssemblySearchPath_UseAssemblyFolders
como false. - A procura no GAC é desabilitada definindo a propriedade
AssemblySearchPath_UseGAC
como false. - Tratar o Include da referência como um nome de arquivo real é desabilitado definindo a propriedade
AssemblySearchPath_UseRawFileName
como false. - A verificação da pasta de saída do aplicativo é desabilitada definindo a propriedade
AssemblySearchPath_UseOutDir
como false.
Houve um conflito
Uma situação comum é que o MSBuild fornece um aviso sobre versões diferentes do mesmo assembly sendo usadas por referências diferentes. A solução geralmente envolve a adição de um redirecionamento de associação ao arquivo app.config.
Uma maneira útil de investigar esses conflitos é pesquisar no Visualizador de Log Estruturado do MSBuild por "Houve um conflito". Ele mostra informações detalhadas sobre quais referências precisavam de quais versões do assembly em questão.