Como o MSBuild compila projetos

Como o MSBuild realmente funciona? Neste artigo você saberá como o MSBuild processa seus arquivos de projeto, sejam invocados do Visual Studio ou de uma linha de comando ou um script. Saber como o MSBuild funciona pode ajudar você a diagnosticar melhor os problemas e personalizar melhor o processo de build. Este artigo descreve o processo de build e é aplicável basicamente a todos os tipos de projeto.

O processo de build completo consiste em inicialização, avaliação e execução iniciais dos destinos e tarefas que compilam o projeto. Além dessas entradas, as importações externas definem os detalhes do processo de build, incluindo importações padrão, como Microsoft.Common.targets e importações configuráveis pelo usuário no nível da solução ou do projeto.

Inicialização

O MSBuild pode ser invocado a partir do Visual Studio por meio do modelo de objeto do MSBuild em Microsoft.Build.dllou invocando o executável diretamente na linha de comando ou em um script, como em sistemas de CI. Em ambos os casos, as entradas que afetam o processo de build incluem o arquivo de projeto (ou objeto de projeto interno para o Visual Studio), possivelmente um arquivo de solução, variáveis de ambiente e comutadores de linha de comando ou seus equivalentes de modelo de objeto. Durante a fase de inicialização, as opções de linha de comando ou os equivalentes do modelo de objeto são usados para definir as configurações do MSBuild, como configurar agentes. As propriedades definidas na linha de comando usando o comutador -property ou -p são definidas como propriedades globais, que substituem todos os valores que seriam definidos nos arquivos de projeto, mesmo que os arquivos de projeto sejam lidos posteriormente.

As próximas seções abordam os arquivos de entrada, como arquivos de solução ou arquivos de projeto.

Soluções e projetos

As instâncias do MSBuild podem consistir em um projeto ou muitos projetos como parte de uma solução. O arquivo de solução não é um arquivo XML do MSBuild, mas o MSBuild o interpreta para conhecer todos os projetos que precisam ser compilados para as configurações fornecidas e as configurações de plataforma. Quando o MSBuild processa essa entrada XML, ele é chamado de build de solução. Ele tem alguns pontos extensíveis que permitem executar algo em cada build de solução, mas como esse build é uma execução separada das compilações individuais do projeto, as configurações de propriedades ou definições de destino do build de solução não são relevantes para cada build de projeto.

Você pode descobrir como estender o build de solução em Personalizar o build de solução.

Builds do Visual Studio versus builds do MSBuild.exe

Há algumas diferenças significativas entre quando os projetos são compilados no Visual Studio e quando você invoca o MSBuild diretamente, por meio do executável do MSBuild ou quando você usa o modelo de objeto do MSBuild para iniciar um build. O Visual Studio gerencia a ordem de build de projeto para builds do Visual Studio. Ele chama apenas o MSBuild no nível do projeto individual e, quando o faz, algumas propriedades boolianas (BuildingInsideVisualStudio, BuildProjectReferences) são definidas afetando significativamente o que o MSBuild faz. Dentro de cada projeto, a execução ocorre da mesma forma que quando invocada por meio do MSBuild, mas a diferença surge com projetos referenciados. No MSBuild, quando os projetos referenciados são necessários, um build realmente ocorre, ou seja, ele executa tarefas e ferramentas e gera a saída. Quando um build do Visual Studio encontra um projeto referenciado, o MSBuild retorna apenas as saídas esperadas do projeto referenciado. Ele permite que o Visual Studio controle a compilação desses outros projetos. O Visual Studio determina a ordem de build e as chamadas para o MSBuild separadamente (conforme necessário), tudo completamente sob o controle do Visual Studio.

Outra diferença surge quando o MSBuild é invocado com um arquivo de solução. O MSBuild analisa o arquivo de solução, cria um arquivo de entrada XML padrão e o avalia e executa como um projeto. O build de solução é executado antes de qualquer projeto. Ao compilar no Visual Studio, nada disso acontece. O MSBuild nunca vê o arquivo de solução. Como consequência, a personalização de build de solução (usando before.SolutionName.sln.targets e after.SolutionName.sln.targets) só se aplica ao MSBuild.exe ou modelo de objeto controlado, e não aos builds do Visual Studio.

SDKs do projeto

O recurso do SDK para arquivos de projeto do MSBuild é relativamente novo. Antes dessa alteração, os arquivos de projeto importavam explicitamente os arquivos .targets e .props que definiam o processo de build para um tipo de projeto específico.

Os projetos do .NET Core importam a versão do SDK do .NET apropriada para eles. Confira a visão geral, os SDKs de projeto do .NET Core e a referência às propriedades.

Fase de avaliação

Esta seção discute como esses arquivos de entrada são processados e analisados para produzir objetos carregados na memória que determinam o que será compilado.

A finalidade da fase de avaliação é criar as estruturas de objeto na memória com base nos arquivos XML de entrada e no ambiente local. A fase de avaliação consiste em seis passagens que processam os arquivos de entrada, como os arquivos XML de projeto, e os arquivos XML importados, geralmente nomeados como arquivos .props ou .targets, dependendo de se eles definem principalmente propriedades ou destinos de build. Cada passagem compila uma parte dos objetos carregados na memória que são usados posteriormente na fase de execução para compilar os projetos, mas nenhuma ação de build real ocorre durante a fase de avaliação. Dentro de cada passagem, os elementos são processados na ordem em que aparecem.

As passagens na fase de avaliação são as seguintes:

  • Avaliar variáveis de ambiente
  • Avaliar importações e propriedades
  • Avaliar definições de item
  • Avaliar itens
  • Avaliar elementos UsingTask
  • Avaliar destinos

A ordem dessas passagens tem implicações significativas e é importante saber quando personalizar o arquivo de projeto. Confira Ordem de avaliação de itens e propriedades.

Avaliar variáveis de ambiente

Nesta fase, as variáveis de ambiente são usadas para definir as propriedades equivalentes. Por exemplo, a variável de ambiente PATH é disponibilizada como uma propriedade $(PATH). Quando executado na linha de comando ou em um script, o ambiente de comando é usado como de costume e, quando executado no Visual Studio, é usado o ambiente estabelecido quando o Visual Studio é iniciado.

Avaliar importações e propriedades

Nesta fase, todo o XML de entrada é lido, incluindo os arquivos de projeto e toda a cadeia de importações. O MSBuild cria uma estrutura XML carregada na memória que representa o XML do projeto e todos os arquivos importados. No momento, as propriedades que não estão em destinos são avaliadas e definidas.

Como o MSBuild lê todos os arquivos de entrada XML no início do processo, as alterações nessas entradas durante o processo de build não afetam o build atual.

As propriedades fora de qualquer destino são tratadas de forma diferente das propriedades dentro dos destinos. Nesta fase, somente as propriedades definidas fora de qualquer destino são avaliadas.

Como as propriedades são processadas em ordem na passagem de propriedades, uma propriedade em qualquer ponto da entrada pode acessar os valores de propriedade que aparecem anteriormente na entrada, mas não as propriedades que aparecem posteriormente.

Como as propriedades são processadas antes que os itens sejam avaliados, você não pode acessar o valor os itens durante qualquer parte da passagem de propriedades.

Avaliar definições de item

Nesta fase, as definições de item são interpretadas e uma representação carregada na memória dessas definições é criada.

Avaliar itens

Os itens definidos dentro de um destino são tratados de forma diferente dos itens fora de qualquer destino. Nesta fase, os itens fora de qualquer destino e seus metadados associados são processados. Os metadados ajustados por definições de item são substituídos pelos metadados ajustados nos itens. Como os itens são processados na ordem em que aparecem, você pode referenciar os itens que foram definidos anteriormente, mas não os que aparecem posteriormente. Como a passagem de itens é após a passagem de propriedades, os itens podem acessar as propriedades se definidos fora de qualquer destino, independentemente de se a definição de propriedade aparece posteriormente.

Avaliar UsingTask elementos

Nesta fase, os elementos UsingTask são lidos e as tarefas são declaradas para uso posterior durante a fase de execução.

Avaliar destinos

Nesta fase, todas as estruturas de objeto de destino são criadas na memória, em preparação para execução. Nenhuma execução real ocorre.

Fase de execução

Na fase de execução, os destinos são ordenados e executados e todas as tarefas são executadas. Mas primeiro, as propriedades e os itens definidos dentro dos destinos são avaliados em conjunto em uma única fase na ordem em que são exibidos. A ordem de processamento é particularmente diferente de como as propriedades e os itens que não estão em um destino são processados: todas as propriedades primeiro e, em seguida, todos os itens, em passagens separadas. As alterações em propriedades e itens dentro de um destino podem ser observadas após o destino em que foram alterados.

Ordem de build de destino

Em um único projeto, os destinos são executados em série. O problema central é como determinar em que ordem compilar tudo de modo que as dependências sejam usadas para compilar os destinos na ordem certa.

A ordem de build de destino é determinada pelo uso dos atributos BeforeTargets, DependsOnTargets e AfterTargets em cada destino. A ordem dos destinos posteriores pode ser influenciada durante a execução de um destino anterior, se o destino anterior modificar uma propriedade referenciada nesses atributos.

As regras para ordenação são descritas em Determinar a ordem de build de destino. O processo é determinado por uma estrutura de pilha que contém os destinos a serem compilados. O destino na parte superior dessa tarefa inicia a execução e, se depender de qualquer outra coisa, esses destinos são enviados para a parte superior da pilha e começam a ser executados. Quando há um destino sem dependências, ele é executado até a conclusão e seu destino pai é retomado.

Referências de Projeto

Há dois caminhos de código que o MSBuild pode tomar, o normal que é descrito aqui e a opção de grafo descrita na próxima seção.

Os projetos individuais especificam a dependência de outros projetos por meio dos itens ProjectReference. Quando um projeto na parte superior da pilha começa a ser compilado, ele atinge o ponto em que o destino ResolveProjectReferences é executado, um destino padrão definido nos arquivos de destino comuns.

ResolveProjectReferences invoca a tarefa do MSBuild com as entradas dos itens ProjectReference para obter as saídas. Os itens ProjectReference são transformados em itens locais, como Reference. A fase de execução do MSBuild para o projeto atual é pausada, enquanto a fase de execução começa a processar o projeto referenciado (a fase de avaliação é feita primeiro conforme necessário). O projeto referenciado só é compilado depois que você começa a compilar o projeto dependente e, portanto, isso cria uma árvore de compilação de projetos.

O Visual Studio permite a criação de dependências de projeto em arquivos de solução (.sln). As dependências são especificadas no arquivo de solução e só são seguidas ao compilar uma solução ou durante a compilação dentro do Visual Studio. Se você compilar um único projeto, esse tipo de dependência será ignorado. As referências de solução são transformadas pelo MSBuild em itens ProjectReference e, depois disso, são tratadas da mesma maneira.

Opção de grafo

Se você especificar o comutador de build de grafo (-graphBuild ou -graph), o ProjectReference se tornará um conceito de primeira classe usado pelo MSBuild. O MSBuild analisará todos os projetos e criará o grafo de ordem de compilação, um grafo de dependência real de projetos, que será então analisado para determinar a ordem de compilação. Assim como acontece com os destinos em projetos individuais, o MSBuild garante que os projetos referenciados sejam compilados após os projetos dos quais dependem.

Execução paralela

Se estiver usando suporte a vários processadores (comutador -maxCpuCount ou -m), o MSBuild criará nós, que são processos do MSBuild que usam os núcleos de CPU disponíveis. Cada projeto é enviado para um nó disponível. Dentro de um nó, os builds de projeto individuais são executados em série.

As tarefas podem ser habilitadas para execução paralela definindo uma variável booliana BuildInParallel, que é definida de acordo com o valor da propriedade $(BuildInParallel) no MSBuild. Para as tarefas habilitadas para execução paralela, um agendador de trabalho gerencia os nós e atribui o trabalho aos nós.

Confira Como compilar vários projetos paralelamente com o MSBuild

Importações padrão

Microsoft.Common.props e Microsoft.Common.targets são importados por arquivos de projeto do .NET (explícita ou implicitamente em projetos no estilo SDK) e estão localizados na pasta MSBuild\Current\bin em uma instalação do Visual Studio. Os projetos C++ têm sua própria hierarquia de importações. Confira Internos do MSBuild para projetos C++.

O arquivo Microsoft.Common.props define os padrões que você pode substituir. Ele é importado (explícita ou implicitamente) no início de um arquivo de projeto. Dessa forma, as configurações do projeto aparecem após os padrões, para que os substituam.

O arquivo Microsoft.Common.targets e os arquivos de destino importados definem o processo de build padrão para projetos .NET. Ele também fornece pontos de extensão que você pode usar para personalizar o build.

Na implementação, Microsoft.Common.targets é um wrapper fino que importa o Microsoft.Common.CurrentVersion.targets. Esse arquivo contém configurações para propriedades padrão e define os destinos reais que definem o processo de build. O destino Build é definido aqui, mas na verdade está vazio. No entanto, o destino Build contém o atributo DependsOnTargets que especifica os destinos individuais que compõem as etapas de build reais, que são BeforeBuild, CoreBuild e AfterBuild. O destino Build é definido da seguinte maneira:

  <PropertyGroup>
    <BuildDependsOn>
      BeforeBuild;
      CoreBuild;
      AfterBuild
    </BuildDependsOn>
  </PropertyGroup>

  <Target
      Name="Build"
      Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
      DependsOnTargets="$(BuildDependsOn)"
      Returns="@(TargetPathWithTargetPlatformMoniker)" />

BeforeBuild e AfterBuild são pontos de extensão. Eles estão vazios no arquivo Microsoft.Common.CurrentVersion.targets, mas os projetos podem fornecer seus próprios destinos BeforeBuild e tarefas AfterBuild que precisam ser executadas antes ou depois do processo de build principal. AfterBuild é executado antes do destino no-op, Build, porque AfterBuild aparece no atributo DependsOnTargets no destino Build, mas ocorre após CoreBuild.

O destino CoreBuild contém as chamadas para as ferramentas de build da seguinte maneira:

  <PropertyGroup>
    <CoreBuildDependsOn>
      BuildOnlySettings;
      PrepareForBuild;
      PreBuildEvent;
      ResolveReferences;
      PrepareResources;
      ResolveKeySource;
      Compile;
      ExportWindowsMDFile;
      UnmanagedUnregistration;
      GenerateSerializationAssemblies;
      CreateSatelliteAssemblies;
      GenerateManifests;
      GetTargetPath;
      PrepareForRun;
      UnmanagedRegistration;
      IncrementalClean;
      PostBuildEvent
    </CoreBuildDependsOn>
  </PropertyGroup>
  <Target
      Name="CoreBuild"
      DependsOnTargets="$(CoreBuildDependsOn)">

    <OnError ExecuteTargets="_TimeStampAfterCompile;PostBuildEvent" Condition="'$(RunPostBuildEvent)'=='Always' or '$(RunPostBuildEvent)'=='OnOutputUpdated'"/>
    <OnError ExecuteTargets="_CleanRecordFileWrites"/>

  </Target>

A tabela a seguir descreve esses destinos. Alguns destinos são aplicáveis somente a determinados tipos de projeto.

Destino Descrição
BuildOnlySettings Configurações somente para builds reais, não para quando o MSBuild é invocado na carga do projeto pelo Visual Studio.
PrepareForBuild Preparar os pré-requisitos para compilação
PreBuildEvent Ponto de extensão para projetos definirem tarefas a serem executadas antes do build
ResolveProjectReferences Analisar dependências de projeto e compilar projetos referenciados
ResolveAssemblyReferences Localizar assemblies referenciados.
ResolveReferences Consiste em ResolveProjectReferences e ResolveAssemblyReferences para localizar todas as dependências
PrepareResources Processar arquivos de recursos
ResolveKeySource Resolva a chave de nome forte usada para assinar o assembly e o certificado usado para assinar os manifestos do ClickOnce.
Compilar Invoca o compilador
ExportWindowsMDFile Gere um arquivo WinMD dos arquivos WinMDModule gerados pelo compilador.
UnmanagedUnregistration Remover/limpar as entradas de registro de Interoperabilidade COM de um build anterior
GenerateSerializationAssemblies Gere um assembly de serialização XML usando o sgen.exe.
CreateSatelliteAssemblies Crie um assembly satélite para cada cultura exclusiva nos recursos.
Gerar manifestos Gera manifestos de implantação e aplicativo ClickOnce ou um manifesto nativo.
GetTargetPath Retorne um item que contém o produto de build (executável ou assembly) deste projeto, com metadados.
PrepareForRun Copie as saídas de build para o diretório final, se elas tiverem sido alteradas.
UnmanagedRegistration Definir entradas de registro para Interoperabilidade COM
IncrementalClean Remova os arquivos produzidos em um build anterior, mas que não foram produzidos no build atual. Isso é necessário para fazer com que Clean funcione em builds incrementais.
PostBuildEvent Ponto de extensão para projetos definirem tarefas a serem executadas após o build

Muitos dos destinos na tabela anterior são encontrados em importações específicas de linguagem, como Microsoft.CSharp.targets. Esse arquivo define as etapas no processo de build padrão específico para projetos C# .NET. Por exemplo, ele contém o destino Compile que realmente chama o compilador C#.

Importações configuráveis pelo usuário

Além das importações padrão, há várias importações que você pode adicionar para personalizar o processo de build.

  • Directory.Build.props
  • Directory.Build.targets

Esses arquivos são lidos pelas importações padrão para projetos em subpastas. Isso geralmente está no nível da solução para que as configurações controlem todos os projetos na solução, mas também podem estar em níveis superiores no sistema de arquivos, até a raiz da unidade.

O arquivo Directory.Build.props é importado por Microsoft.Common.props. Portanto, as propriedades definidas nele estão disponíveis no arquivo de projeto. Elas podem ser redefinidas no arquivo de projeto para personalizar os valores por projeto. O arquivo Directory.Build.targets é lido após o arquivo de projeto. Normalmente, ele contém destinos, mas aqui você também pode definir as propriedades que não deseja que os projetos individuais redefinam.

Personalizações em um arquivo de projeto

O Visual Studio atualiza seus arquivos de projeto, à medida que você faz alterações no Gerenciador de Soluções, na janela Propriedades ou nas Propriedades do Projeto, mas você também pode fazer suas próprias alterações editando diretamente o arquivo de projeto.

Muitos comportamentos de build podem ser configurados definindo as propriedades do MSBuild, seja no arquivo de projeto para configurações locais de um projeto ou conforme mencionado na seção anterior, criando um arquivo Directory.Build.props para definir propriedades globalmente para pastas inteiras de projetos e soluções. Para compilações ad hoc na linha de comando ou em scripts, você também pode usar a opção /p na linha de comando para definir propriedades para uma invocação específica do MSBuild. Confira Propriedades comuns do projeto do MSBuild para obter informações sobre as propriedades que você pode definir.