Estender o processo de build do Visual Studio

O processo de build do Visual Studio é definido por uma série de arquivos .targets do MSBuild que são importados para o arquivo de projeto. Essas importações são implícitas ao usar um SDK, como é comum nos projetos do Visual Studio. Um desses arquivos importados, Microsoft.Common.targets, pode ser estendido para permitir a execução de tarefas personalizadas em vários pontos no processo de build. Este artigo explica os três métodos que você pode usar para estender o processo de build do Visual Studio:

  • Criar um destino personalizado e especifique quando ele deve ser executado usando atributos BeforeTargets e AfterTargets.

  • Substituir as propriedades DependsOn definidas nos destinos comuns.

  • Substituir destinos predefinidos específicos definidos nos destinos comuns (Microsoft.Common.targets ou nos arquivos importados por ele).

AfterTargets e BeforeTargets

Você pode usar atributos AfterTargets e BeforeTargets no destino personalizado para especificar quando ele deve ser executado.

O exemplo a seguir mostra como usar o atributo AfterTargets para adicionar um destino personalizado que faz algo com os arquivos de saída. Nesse caso, ele copia os arquivos de saída para uma nova pasta CustomOutput. O exemplo também mostra como limpar os arquivos criados pela operação de build personalizada com um destino CustomClean usando um atributo BeforeTargets e especificando que a operação de limpeza personalizada é executada antes do destino CoreClean.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
     <TargetFramework>netcoreapp3.1</TargetFramework>
     <_OutputCopyLocation>$(OutputPath)..\..\CustomOutput\</_OutputCopyLocation>
  </PropertyGroup>

  <Target Name="CustomAfterBuild" AfterTargets="Build">
    <ItemGroup>
      <_FilesToCopy Include="$(OutputPath)**\*"/>
    </ItemGroup>
    <Message Text="_FilesToCopy: @(_FilesToCopy)" Importance="high"/>

    <Message Text="DestFiles:
        @(_FilesToCopy->'$(_OutputCopyLocation)%(RecursiveDir)%(Filename)%(Extension)')"/>

    <Copy SourceFiles="@(_FilesToCopy)"
          DestinationFiles=
          "@(_FilesToCopy->'$(_OutputCopyLocation)%(RecursiveDir)%(Filename)%(Extension)')"/>
  </Target>

  <Target Name="CustomClean" BeforeTargets="CoreClean">
    <Message Text="Inside Custom Clean" Importance="high"/>
    <ItemGroup>
      <_CustomFilesToDelete Include="$(_OutputCopyLocation)**\*"/>
    </ItemGroup>
    <Delete Files='@(_CustomFilesToDelete)'/>
  </Target>
</Project>

Aviso

Use nomes diferentes dos destinos predefinidos (por exemplo, o destino de build personalizado aqui é CustomAfterBuild, não AfterBuild), já que esses destinos predefinidos são substituídos pela importação do SDK que também os define. Confira a tabela no final deste artigo para obter uma lista de destinos predefinidos.

Estender as propriedades DependsOn

Outra maneira de estender o processo de build é usar as propriedades DependsOn (por exemplo, BuildDependsOn), para especificar destinos que devem ser executados antes de um destino padrão.

Esse método é preferível à substituição de destinos predefinidos discutida na próxima seção. A substituição de destinos predefinidos é um método antigo que ainda tem suporte. No entanto, como o MSBuild avalia a definição de destinos sequencialmente, não há nenhuma maneira de impedir que outro projeto que importa seu projeto substitua os destinos já substituídos por você. Dessa forma, por exemplo, o último destino AfterBuild no arquivo de projeto, depois que todos os outros projetos foram importados, será aquele usado durante o build.

Você pode se proteger contra substituições indesejadas de destinos substituindo as propriedades DependsOn usadas em atributos DependsOnTargets em todo o arquivo de destinos comuns. Por exemplo, o destino Build contém um valor de atributo DependsOnTargets de "$(BuildDependsOn)". Considere:

<Target Name="Build" DependsOnTargets="$(BuildDependsOn)"/>

Este trecho de XML indica que, antes de poder executar o destino Build, todos os destinos especificados na propriedade BuildDependsOn devem ser executados primeiro. A propriedade BuildDependsOn está definida como:

<PropertyGroup>
    <BuildDependsOn>
        $(BuildDependsOn);
        BeforeBuild;
        CoreBuild;
        AfterBuild
    </BuildDependsOn>
</PropertyGroup>

Você pode substituir esse valor da propriedade declarando outra propriedade denominada BuildDependsOn no final do seu arquivo de projeto. Em um projeto no estilo SDK, isso significa que você precisa usar importações explícitas. Confira Importações implícitas e explícitas, para colocar a propriedade DependsOn após a última importação. Incluindo a propriedade BuildDependsOn anterior na nova propriedade, você pode adicionar novos destinos no início e fim da lista de destinos. Por exemplo:

<PropertyGroup>
    <BuildDependsOn>
        MyCustomTarget1;
        $(BuildDependsOn);
        MyCustomTarget2
    </BuildDependsOn>
</PropertyGroup>

<Target Name="MyCustomTarget1">
    <Message Text="Running MyCustomTarget1..."/>
</Target>
<Target Name="MyCustomTarget2">
    <Message Text="Running MyCustomTarget2..."/>
</Target>

Projetos que importam seu arquivo de projeto podem estender essas propriedades sem substituir as personalizações que você fez.

Para substituir uma propriedade DependsOn

  1. Identifique uma propriedade DependsOn predefinida no arquivo de destinos comuns que você deseja substituir. Confira a tabela a seguir para obter uma lista das propriedades DependsOn geralmente substituídas.

  2. Defina outra instância da propriedade ou propriedades no final do seu arquivo de projeto. Inclua a propriedade original, por exemplo $(BuildDependsOn), na nova propriedade.

  3. Defina seus destinos personalizados antes ou após a definição da propriedade.

  4. Compile o arquivo de projeto.

Propriedades DependsOn geralmente substituídas

Nome da propriedade Os destinos adicionados são executados antes deste ponto:
BuildDependsOn O ponto de entrada de build principal. Substitua esta propriedade se você quiser inserir destinos personalizados antes ou após o processo inteiro de build.
RebuildDependsOn O Rebuild
RunDependsOn A execução da saída de build final (se for um .EXE)
CompileDependsOn A compilação (destino Compile). Substitua esta propriedade se você quiser inserir processos personalizados antes ou após a etapa de compilação.
CreateSatelliteAssembliesDependsOn A criação das assemblies satélites
CleanDependsOn O destino Clean (Exclusão de todas as saídas de compilação intermediárias e finais). Substitua esta propriedade se você quiser limpar a saída do processo de build personalizado.
PostBuildEventDependsOn O destino PostBuildEvent
PublishBuildDependsOn Publicação de build
ResolveAssemblyReferencesDependsOn O destino ResolveAssemblyReferences (encontrar o fechamento transitivo de dependências para uma dependência). Consulte ResolveAssemblyReference.

Exemplo: BuildDependsOn e CleanDependsOn

O exemplo a seguir é semelhante ao exemplo BeforeTargets e AfterTargets, mas mostra como obter uma funcionalidade semelhante. Ele estende o build usando BuildDependsOn para adicionar sua tarefa CustomAfterBuild, que copia os arquivos de saída após o build, e também adiciona a tarefa CustomClean correspondente usando CleanDependsOn.

Neste exemplo, este é um projeto no estilo SDK. Conforme mencionado na nota sobre projetos no estilo SDK anteriormente neste artigo, você precisa usar o método de importação manual em vez do atributo Sdk que o Visual Studio usa quando gera arquivos de projeto.

<Project>
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk"/>

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk"/>

  <PropertyGroup>
    <BuildDependsOn>
      $(BuildDependsOn);CustomAfterBuild
    </BuildDependsOn>

    <CleanDependsOn>
      $(CleanDependsOn);CustomClean
    </CleanDependsOn>

    <_OutputCopyLocation>$(OutputPath)..\..\CustomOutput\</_OutputCopyLocation>
  </PropertyGroup>

  <Target Name="CustomAfterBuild">
    <ItemGroup>
      <_FilesToCopy Include="$(OutputPath)**\*"/>
    </ItemGroup>
    <Message Importance="high" Text="_FilesToCopy: @(_FilesToCopy)"/>

    <Message Text="DestFiles:
      @(_FilesToCopy-&gt;'$(_OutputCopyLocation)%(RecursiveDir)%(Filename)%(Extension)')"/>

    <Copy SourceFiles="@(_FilesToCopy)"
          DestinationFiles="@(_FilesToCopy-&gt;'$(_OutputCopyLocation)%(RecursiveDir)%(Filename)%(Extension)')"/>
  </Target>

  <Target Name="CustomClean">
    <Message Importance="high" Text="Inside Custom Clean"/>
    <ItemGroup>
      <_CustomFilesToDelete Include="$(_OutputCopyLocation)**\*"/>
    </ItemGroup>
    <Delete Files="@(_CustomFilesToDelete)"/>
  </Target>
</Project>

A ordem dos elementos é importante. Os elementos BuildDependsOn e CleanDependsOn precisam aparecer depois de importar o arquivo de destinos padrão do SDK.

Substituir destinos predefinidos

Os arquivos de .targets comuns contém um conjunto de destinos vazios predefinidos que são chamados antes e depois de alguns dos principais destinos no processo de build. Por exemplo, o MSBuild chama o destino BeforeBuild antes do destino principal CoreBuild e o destino AfterBuild após o destino CoreBuild. Por padrão, os destinos vazios no arquivo de destinos comuns não têm nenhum efeito, mas você pode substituir o comportamento padrão deles definindo os destinos desejados em um arquivo de projeto. Os métodos descritos anteriormente neste artigo são preferenciais, mas códigos mais antigos podem usar esse método.

Quando o projeto usa um SDK (por exemplo Microsoft.Net.Sdk), é necessário alterar as importações implícitas para explícitas, conforme discutido em Importações implícitas e explícitas.

Para substituir um destino predefinido

  1. Quando o projeto usa o atributo Sdk, altere-o para a sintaxe de importação explícita. Confira Importações implícitas e explícitas.

  2. Identifique um destino predefinido no arquivo de destinos comuns que você deseja substituir. Confira a tabela abaixo para obter uma lista completa de destinos que podem ser substituídos com segurança.

  3. Defina o destino ou destinos no final do arquivo de projeto, imediatamente antes da marca </Project> e após a importação de SDK explicita. Por exemplo:

    <Project>
        <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
        ...
        <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
        <Target Name="BeforeBuild">
            <!-- Insert tasks to run before build here -->
        </Target>
        <Target Name="AfterBuild">
            <!-- Insert tasks to run after build here -->
        </Target>
    </Project>
    

    Observe que o atributo Sdk no elemento de nível superior Project foi removido.

  4. Compile o arquivo de projeto.

Tabela de destinos predefinidos

A tabela a seguir mostra todos os destinos no arquivo de destinos comuns que você pode substituir.

Nome de destino Descrição
BeforeCompile, AfterCompile As tarefas inseridas em um desses destinos são executadas antes ou após a conclusão da compilação principal. A maioria das personalizações é realizada em um desses dois destinos.
BeforeBuild, AfterBuild As tarefas inseridas em um desses destinos serão executadas antes ou depois de todo o resto no build. Observação: os destinos BeforeBuild e AfterBuild já estão definidos nos comentários ao final da maioria dos arquivos de projeto, permitindo que você adicione com facilidade eventos de pré e pós-build ao arquivo de projeto.
BeforeRebuild, AfterRebuild As tarefas inseridas em um desses destinos são executadas antes ou após a invocação da funcionalidade de recompilação principal. A ordem de execução de destino em Microsoft.Common.targets é: BeforeRebuild, Clean, Build e, em seguida, AfterRebuild.
BeforeClean, AfterClean As tarefas inseridas em um desses destinos são executadas antes ou após a invocação da funcionalidade de limpeza principal.
BeforePublish, AfterPublish As tarefas inseridas em um desses destinos são executadas antes ou após a invocação da funcionalidade de publicação principal.
BeforeResolveReferences, AfterResolveReferences As tarefas inseridas em um desses destinos são executadas antes ou após a resolução das referências de assembly.
BeforeResGen, AfterResGen As tarefas inseridas em um desses destinos são executadas antes ou após a geração de recursos.

Há muito mais destinos no sistema de build e no SDK do .NET, confira Destinos do MSBuild - SDK e destinos de build padrão.

As melhores práticas para destinos personalizados

As propriedades DependsOnTargets e BeforeTargets podem especificar se um destino deve ser executado antes de outro destino, mas ambas são necessárias em cenários diferentes. Elas diferem em relação ao destino em que o requisito de dependência é especificado. Você só tem controle sobre os próprios destinos e não pode modificar com segurança os destinos do sistema ou outros destinos importados, de modo que isso restringe a escolha dos métodos.

Ao criar um destino personalizado, siga estas diretrizes gerais para garantir que o destino seja executado na ordem desejada.

  1. Use o atributo DependsOnTargets para especificar os destinos que devem ser concluídos antes da execução do destino. Para cadeias de destinos que você controla, cada destino pode especificar o membro anterior da cadeia em DependsOnTargets.

  2. Use BeforeTargets para qualquer destino que você não controla e deve executar antes (como BeforeTargets="PrepareForBuild" para um destino que precisa ser executado no início do processo de build).

  3. Use AfterTargets para qualquer destino que você não controla e que garante que as saídas que você precisa estão disponíveis. Por exemplo, especifique AfterTargets="ResolveReferences" para algo que modificará uma lista de referências.

  4. Você pode usar qualquer combinação. Por exemplo, DependsOnTargets="GenerateAssemblyInfo" BeforeTargets="BeforeCompile".

Importações implícitas e explícitas

Os projetos gerados pelo Visual Studio costumam usar o atributo Sdk no elemento do projeto. Esses tipos de projetos são chamados de projetos no estilo SDK. Confira Como usar SDKs de projeto do MSBuild. Veja um exemplo:

<Project Sdk="Microsoft.Net.Sdk">

Quando o projeto usa o atributo Sdk, duas importações são adicionadas implicitamente, uma no início do arquivo de projeto e outra no final.

As importações implícitas equivalem a ter uma instrução de importação como esta como a primeira linha no arquivo de projeto, após o elemento Project:

<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

e a seguinte instrução de importação como a última linha no arquivo de projeto:

<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />

Essa sintaxe é chamada de importações de SDK explícitas. Ao usar essa sintaxe explícita, você deve omitir o atributo Sdk no elemento do projeto.

A importação de SDK implícita é equivalente à importação de arquivos “comuns” .props ou .targets específicos, uma construção típica em arquivos de projeto mais antigos, como por exemplo:

<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />

e

<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

Essas referências antigas devem ser substituídas pela sintaxe de SDK explícita mostrada anteriormente nesta seção.

O uso da sintaxe de SDK explícita significa que você pode adicionar seu próprio código antes da primeira importação ou após a importação de SDK final. Isso significa que é possível alterar o comportamento ao definir as propriedades antes da primeira importação que entrará em vigor no arquivo importado .props e que você pode substituir um destino definido em um dos arquivos do SDK .targets após a importação final. Você pode substituir BeforeBuild ou AfterBuild usando esse método, conforme discutido a seguir.

Próximas etapas

Você pode fazer muito mais com o MSBuild para personalizar o build. Confira Personalizar seu build.