Personalizar o build por pasta

Você pode adicionar determinados arquivos a serem importados pelo MSBuild para substituir as configurações de propriedade padrão e adicionar destinos personalizados. O escopo dessas personalizações pode ser controlado no nível da pasta pelo local em que esses arquivos são colocados.

Este artigo aborda as personalizações aplicáveis aos seguintes cenários:

  • Personalizar as configurações de build para muitos projetos em uma solução
  • Personalizar as configurações de build para muitas soluções em um diretório de arquivos comum
  • Personalizar configurações de build que podem ser diferentes para subpastas em uma estrutura complexa de pastas
  • Substituir configurações padrão, pastas de build padrão e outros comportamentos definidos por um SDK, como Microsoft.Net.Sdk
  • Adicionar ou personalizar destinos de build que se aplicam a qualquer número de projetos ou soluções

Se você estiver trabalhando com projetos C++, também poderá usar os métodos descritos em Personalizar compilações C++.

Directory.Build.props e Directory.Build.targets

Você pode adicionar uma nova propriedade a todos os projetos, definindo-a em um único arquivo chamado Directory.Build.props na pasta raiz que contém sua fonte.

Quando o MSBuild é executado, o Microsoft.Common.props pesquisa sua estrutura de diretório pelo arquivo Directory.Build.props. Se encontrar um, ele importará o arquivo e lerá as propriedades definidas nele. Directory.Build.props é um arquivo definido pelo usuário que fornece personalizações de projetos em um diretório.

Da mesma forma, Microsoft.Common.targets procura Directory.Build.targets.

Directory.Build.props é importado no início da sequência de arquivos importados, o que pode ser importante se você precisar definir uma propriedade que é usada por importações, especialmente aquelas que são importadas implicitamente usando o atributo Sdk, como ao usar o SDK do .NET na maioria dos arquivos de projeto do .NET.

Observação

Sistemas de arquivos baseados em Linux diferenciam maiúsculas de minúsculas. Certifique-se de que as maiúsculas e minúsculas do nome do arquivo Directory.Build.props correspondam exatamente, ou ele não será detectado durante o processo de build.

Saiba mais neste tópico do GitHub.

Exemplo de Directory.Build.props

Por exemplo, este é um arquivo Directory.Build.props que define o diretório de saída para todos os projetos em uma solução Visual Studio. A saída de cada projeto é colocada sob seu próprio nome de projeto. Neste exemplo, o arquivo Directory.Build.props está em uma pasta de solução, com muitos projetos em subpastas abaixo dele. A propriedade $(MSBuildProjectName) dá o nome de cada projeto. Como o arquivo Directory.Build.props é importado para cada projeto durante sua própria compilação, ele é avaliado com o valor correto para cada projeto individual na solução.

  1. Limpe a solução para remover todos os arquivos de saída antigos.

    msbuild /t:Clean SolutionName.sln

  2. Crie um novo arquivo na raiz do seu repositório chamado Directory.Build.props.

  3. Adicione o XML a seguir ao arquivo.

    <Project>
       <PropertyGroup>
          <OutDir>C:\output\$(MSBuildProjectName)</OutDir>
       </PropertyGroup>
    </Project>
    

    Observação

    A propriedade $(OutDir) é um caminho absoluto para a saída e usá-la ignora a criação de subpastas para a configuração, estrutura de destino ou runtime que normalmente são usados em projetos .NET. Em vez disso, tente usar a propriedade BaseOutputPath se quiser que as subpastas comuns sejam criadas em um caminho de saída personalizado.

  4. Execute o MSBuild. As importações existentes do seu projeto de Microsoft.Common.props e Microsoft.Common.targets localizam o arquivo Directory.Build.props e o importam, e a nova pasta de saída é usada para todos os projetos nessa pasta.

Escopo da pesquisa

Ao pesquisar um arquivo Directory.Build.props, o MSBuild orienta a estrutura do diretório para cima do local $(MSBuildProjectFullPath)do projeto, parando depois de localizar um arquivo Directory.Build.props. Por exemplo, se seu $(MSBuildProjectFullPath) for c:\usuários\nomedeusuário\código\teste\caso1, o MSBuild deve iniciar a pesquisa aí e depois pesquisar a estrutura do diretório para cima até localizar um arquivo Directory.Build.props, como na estrutura de diretório a seguir.

c:\users\username\code\test\case1
c:\users\username\code\test
c:\users\username\code
c:\users\username
c:\users
c:\

O local do arquivo de solução é irrelevante para o Directory.Build.props.

Ordem de importação

Directory.Build.props é importado no início de Microsoft.Common.props e as propriedades definidas posteriormente não estão disponíveis para ele. Portanto, evite fazer referência a propriedades que ainda não foram definidas (e que serão avaliadas como vazias).

As propriedades definidas em Directory.Build.props podem ser substituídas em outro lugar no arquivo de projeto ou em arquivos importados, portanto, você deve pensar nas configurações em Directory.Build.props como especificando os padrões para seus projetos.

O Directory.Build.targets é importado do Microsoft.Common.targets depois de importar os arquivos .targets dos pacotes do NuGet. Portanto, ele pode substituir propriedades e destinos definidos na maior parte da lógica de build ou definir propriedades para todos os seus projetos, independentemente do que os projetos individuais definem.

Quando é necessário definir uma propriedade ou definir um destino para um projeto individual que substitua as configurações anteriores, coloque essa lógica no arquivo de projeto após a importação final. Para fazer isso em um projeto no estilo SDK, primeiro você precisa substituir o atributo de estilo SDK pelas importações equivalentes. Consulte Como usar SDKs de projeto do MSBuild.

Observação

O mecanismo do MSBuild lê todos os arquivos importados durante a avaliação, antes de iniciar a execução do build para um projeto (incluindo qualquer PreBuildEvent), portanto, não é esperado que esses arquivos sejam modificados por PreBuildEvent ou por qualquer outra parte do processo de build. As modificações não entrarão em vigor até a próxima invocação do MSBuild.exe ou até a próxima compilação do Visual Studio. Além disso, se o processo de build contiver muitos builds de projeto (como quando há vários destinos ou na compilação de projetos dependentes), os arquivos importados, incluindo Directory.build.props, serão lidos quando ocorrer uma avaliação para cada build de projeto individual.

Caso de uso: mesclagem de vários níveis

Suponha que você tenha essa estrutura de solução padrão:

\
  MySolution.sln
  Directory.Build.props     (1)
  \src
    Directory.Build.props   (2-src)
    \Project1
    \Project2
  \test
    Directory.Build.props   (2-test)
    \Project1Tests
    \Project2Tests

Pode ser interessante ter propriedades comuns para todos os projetos (1), propriedades comuns para projetos src(2-src) e propriedades comuns para projetos test(2-test).

Para que o MSBuild mescle corretamente os arquivos "internos" (2-src e 2-test) com o arquivo "externo" (1), você precisa considerar que, depois de o MSBuild localizar um arquivo Directory.Build.props, ele interrompe a verificação adicional. Para continuar a verificação e a mesclagem no arquivo externo, coloque este código em ambos os arquivos internos:

<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

Um resumo da abordagem geral do MSBuild é o seguinte:

  • Para qualquer projeto específico, o MSBuild localiza o primeiro Directory.Build.props para cima na estrutura da solução, mescla-o com padrões e interrompe o exame adicional.
  • Se você quiser que vários níveis sejam encontrados e mesclados, então <Import...> (mostrado anteriormente) o arquivo "externo" do arquivo "interno".
  • Se o arquivo "externo" não importar algo acima dele, a verificação será interrompida.

Ou, de maneira mais simples: o primeiro Directory.Build.props que não importar nada é o local em que o MSBuild para.

Para controlar o processo de importação de forma mais explícita, use as propriedades $(DirectoryBuildPropsPath), $(ImportDirectoryBuildProps), $(DirectoryBuildTargetsPath)e $(ImportDirectoryBuildTargets). A propriedade $(DirectoryBuildPropsPath) especifica o caminho para o arquivo Directory.Build.props a ser usado; da mesma forma, $(DirectoryBuildTargetsPath) especifica o caminho para o arquivo Directory.Build.targets.

As propriedades boolianas $(ImportDirectoryBuildProps) e $(ImportDirectoryBuildTargets) são definidas como true por padrão, portanto, o MSBuild normalmente pesquisa esses arquivos, mas você pode defini-los como false para impedir que o MSBuild os importe.

Exemplo

Este exemplo mostra o uso da saída do arquivo de projeto do MSBuild pré-processado para determinar onde definir uma propriedade.

Para ajudar você analisar o uso de uma propriedade específica que deseja definir, você pode executar o MSBuild com o argumento /preprocess ou /pp. O texto de saída é o resultado de todas as importações, incluindo importações do sistema, como Microsoft.Common.props, que são importadas implicitamente, bem como qualquer uma de suas importações. Com essa saída, você pode ver onde sua propriedade precisa ser definida em relação ao local em que o valor dela é usado.

Por exemplo, suponha que você tenha um projeto simples do .NET Core ou do .NET 5 ou posterior do Aplicativo de Console e queira personalizar a pasta de saída intermediária, normalmente obj. A propriedade que especifica esse caminho é BaseIntermediateOutput. Se você tentar colocar isso em um elemento PropertyGroup em seu arquivo de projeto junto com as várias outras propriedades que já estão definidas nele, como TargetFramework, você descobrirá ao compilar o projeto que a propriedade não entra em vigor. Se você executar o MSBuild com a opção /pp e pesquisar na saída por BaseIntermediateOutputPath, poderá ver o motivo. Nesse caso, BaseIntermediateOutput é lido e usado em Microsoft.Common.props.

Há um comentário em Microsoft.Common.props que informa que a propriedade BaseIntermediateOutput precisa ser definida aqui antes de ser usada por outra propriedade, MSBuildProjectExtensionsPath. Você também pode ver que, quando BaseIntermediateOutputPath é definido inicialmente, há uma verificação de um valor pré-existente e, se ele estiver indefinido, será definido como obj.

<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>

Portanto, esse posicionamento informa que, para definir essa propriedade, ela deve ser especificada para algum momento anterior ao presente. Logo antes desse código na saída pré-processada, você pode ver que Directory.Build.props é importado, portanto, você possa definir BaseIntermediateOutputPath e ele será definido com antecedência suficiente para ter o efeito desejado.

A saída pré-processada abreviada a seguir mostra o resultado de colocar a configuração BaseIntermediateOutput em Directory.Build.props. Os comentários na parte superior das importações padrão incluem o nome do arquivo e, geralmente, algumas informações úteis sobre por que esse arquivo é importado.

<?xml version="1.0" encoding="IBM437"?>
<!--
============================================================================================================================================
c:\source\repos\ConsoleApp9\ConsoleApp9\ConsoleApp9.csproj
============================================================================================================================================
-->
<Project DefaultTargets="Build">
  <!--
============================================================================================================================================
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk">
  This import was added implicitly because the Project element's Sdk attribute specified "Microsoft.NET.Sdk".

C:\Program Files\dotnet\sdk\7.0.200-preview.22628.1\Sdks\Microsoft.NET.Sdk\Sdk\Sdk.props
============================================================================================================================================
-->
  <!--
***********************************************************************************************
Sdk.props

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
          created a backup copy.  Incorrect changes to this file will make it
          impossible to load or build your projects from the command-line or the IDE.

Copyright (c) .NET Foundation. All rights reserved.
***********************************************************************************************
-->
  <PropertyGroup xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <!--
      Indicate to other targets that Microsoft.NET.Sdk is being used.

      This must be set here (as early as possible, before Microsoft.Common.props)
      so that everything that follows can depend on it.

      In particular, Directory.Build.props and nuget package props need to be able
      to use this flag and they are imported by Microsoft.Common.props.
    -->
    <UsingMicrosoftNETSdk>true</UsingMicrosoftNETSdk>
    <!--
      Indicate whether the set of SDK defaults that makes SDK style project concise are being used.
      For example: globbing, importing msbuild common targets.

      Similar to the property above, it must be set here.
    -->
    <UsingNETSdkDefaults>true</UsingNETSdkDefaults>
  </PropertyGroup>
  <PropertyGroup Condition="'$(MSBuildProjectFullPath)' == '$(ProjectToOverrideProjectExtensionsPath)'" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <MSBuildProjectExtensionsPath>$(ProjectExtensionsPathForSpecifiedProject)</MSBuildProjectExtensionsPath>
  </PropertyGroup>
  <!--<Import Project="$(AlternateCommonProps)" Condition="'$(AlternateCommonProps)' != ''" />-->
  <!--
============================================================================================================================================
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="'$(AlternateCommonProps)' == ''">

C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Microsoft.Common.props
============================================================================================================================================
-->
  <!--
***********************************************************************************************
Microsoft.Common.props

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
          created a backup copy.  Incorrect changes to this file will make it
          impossible to load or build your projects from the command-line or the IDE.

Copyright (C) Microsoft Corporation. All rights reserved.
***********************************************************************************************
-->
  <PropertyGroup>
    <ImportByWildcardBeforeMicrosoftCommonProps Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportByWildcardBeforeMicrosoftCommonProps>
    <ImportByWildcardAfterMicrosoftCommonProps Condition="'$(ImportByWildcardAfterMicrosoftCommonProps)' == ''">true</ImportByWildcardAfterMicrosoftCommonProps>
    <ImportUserLocationsByWildcardBeforeMicrosoftCommonProps Condition="'$(ImportUserLocationsByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportUserLocationsByWildcardBeforeMicrosoftCommonProps>
    <ImportUserLocationsByWildcardAfterMicrosoftCommonProps Condition="'$(ImportUserLocationsByWildcardAfterMicrosoftCommonProps)' == ''">true</ImportUserLocationsByWildcardAfterMicrosoftCommonProps>
    <ImportDirectoryBuildProps Condition="'$(ImportDirectoryBuildProps)' == ''">true</ImportDirectoryBuildProps>
  </PropertyGroup>
  <!--
      Determine the path to the directory build props file if the user did not disable $(ImportDirectoryBuildProps) and
      they did not already specify an absolute path to use via $(DirectoryBuildPropsPath)
  -->
  <PropertyGroup Condition="'$(ImportDirectoryBuildProps)' == 'true' and '$(DirectoryBuildPropsPath)' == ''">
    <_DirectoryBuildPropsFile Condition="'$(_DirectoryBuildPropsFile)' == ''">Directory.Build.props</_DirectoryBuildPropsFile>
    <_DirectoryBuildPropsBasePath Condition="'$(_DirectoryBuildPropsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildPropsFile)'))</_DirectoryBuildPropsBasePath>
    <DirectoryBuildPropsPath Condition="'$(_DirectoryBuildPropsBasePath)' != '' and '$(_DirectoryBuildPropsFile)' != ''">$([System.IO.Path]::Combine('$(_DirectoryBuildPropsBasePath)', '$(_DirectoryBuildPropsFile)'))</DirectoryBuildPropsPath>
  </PropertyGroup>
  <!--
============================================================================================================================================
  <Import Project="$(DirectoryBuildPropsPath)" Condition="'$(ImportDirectoryBuildProps)' == 'true' and exists('$(DirectoryBuildPropsPath)')">

c:\source\repos\ConsoleApp9\Directory.Build.props
============================================================================================================================================
-->
  <!-- Directory.build.props
-->
  <PropertyGroup>
    <BaseIntermediateOutputPath>myBaseIntermediateOutputPath</BaseIntermediateOutputPath>
  </PropertyGroup>
  <!--
============================================================================================================================================
  </Import>

C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Microsoft.Common.props
============================================================================================================================================
-->
  <!--
      Prepare to import project extensions which usually come from packages.  Package management systems will create a file at:
        $(MSBuildProjectExtensionsPath)\$(MSBuildProjectFile).<SomethingUnique>.props

      Each package management system should use a unique moniker to avoid collisions.  It is a wild-card import so the package
      management system can write out multiple files but the order of the import is alphabetic because MSBuild sorts the list.
  -->
  <PropertyGroup>
    <!--
        The declaration of $(BaseIntermediateOutputPath) had to be moved up from Microsoft.Common.CurrentVersion.targets
        in order for the $(MSBuildProjectExtensionsPath) to use it as a default.
    -->
    <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>
    <BaseIntermediateOutputPath Condition="!HasTrailingSlash('$(BaseIntermediateOutputPath)')">$(BaseIntermediateOutputPath)\</BaseIntermediateOutputPath>
    <_InitialBaseIntermediateOutputPath>$(BaseIntermediateOutputPath)</_InitialBaseIntermediateOutputPath>
    <MSBuildProjectExtensionsPath Condition="'$(MSBuildProjectExtensionsPath)' == '' ">$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
    <!--
        Import paths that are relative default to be relative to the importing file.  However, since MSBuildExtensionsPath
        defaults to BaseIntermediateOutputPath we expect it to be relative to the project directory.  So if the path is relative
        it needs to be made absolute based on the project directory.
    -->
    <MSBuildProjectExtensionsPath Condition="'$([System.IO.Path]::IsPathRooted($(MSBuildProjectExtensionsPath)))' == 'false'">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)'))</MSBuildProjectExtensionsPath>
    <MSBuildProjectExtensionsPath Condition="!HasTrailingSlash('$(MSBuildProjectExtensionsPath)')">$(MSBuildProjectExtensionsPath)\</MSBuildProjectExtensionsPath>
    <ImportProjectExtensionProps Condition="'$(ImportProjectExtensionProps)' == ''">true</ImportProjectExtensionProps>
    <_InitialMSBuildProjectExtensionsPath Condition=" '$(ImportProjectExtensionProps)' == 'true' ">$(MSBuildProjectExtensionsPath)</_InitialMSBuildProjectExtensionsPath>
  </PropertyGroup>
  ...