Personalización de la compilación por carpeta

Puede agregar ciertos archivos para que MSBuild los importe a fin de invalidar la configuración de propiedades predeterminada y de agregar destinos personalizados. El ámbito de estas personalizaciones se puede controlar en el nivel de carpeta en el que se colocan estos archivos.

En este artículo se tratan las personalizaciones aplicables a los escenarios siguientes:

  • Personalización de la configuración de compilación para muchos proyectos de una solución
  • Personalización de la configuración de compilación para muchas soluciones en un directorio de archivos común
  • Personalización de la configuración de compilación que puede ser distinta para las subcarpetas en una estructura de carpetas compleja
  • Invalidación de la configuración predeterminada, las carpetas de compilación predeterminadas y otros comportamientos establecidos por un SDK, como Microsoft.Net.Sdk
  • Adición o personalización de destinos de compilación aplicables a un número cualquiera de proyectos o soluciones

Si está trabajando con proyectos de C++, consulte también Personalización de compilaciones de C++.

Directory.Build.props y Directory.Build.targets

Se puede agregar una propiedad nueva a cada proyecto si se define en un único archivo denominado Directory.Build.props en la carpeta raíz que contiene el origen.

Cuando se ejecuta MSBuild, Microsoft.Common.props busca su estructura de directorio para el archivo Directory.Build.props. Si encuentra uno, importa el archivo y lee las propiedades definidas en él. Directory.Build.props es un archivo definido por el usuario que proporciona personalizaciones a los proyectos de un directorio.

Del mismo modo, Microsoft.Common.targets busca Directory.Build.targets.

Directory.Build.props se importa al principio de la secuencia de archivos importados, lo que puede ser importante si necesita establecer una propiedad que usan las importaciones, especialmente las que se importan implícitamente mediante el Sdk atributo, como cuando se usa el SDK de .NET en la mayoría de los archivos de proyecto de .NET.

Nota:

Los sistemas de archivos basados en Linux distinguen mayúsculas de minúsculas. Asegúrese de que las mayúsculas y minúsculas del nombre del archivo Directory.Build.props coincidan de manera exacta o no se detectará durante el proceso de compilación.

Para más información, consulte este problema de GitHub.

Ejemplo de Directory.Build.props

Por ejemplo, si quisiera permitir que todos sus proyectos tuvieran acceso a la nueva característica /deterministic de Roslyn (que se expone en el destino CoreCompile de Roslyn mediante la propiedad $(Deterministic)), podría realizar lo siguiente.

  1. Crear un archivo en la raíz de su repositorio denominado Directory.Build.props.

  2. Agregar el siguiente XML al archivo.

    <Project>
     <PropertyGroup>
       <Deterministic>true</Deterministic>
     </PropertyGroup>
    </Project>
    
  3. Ejecutar MSBuild. Las importaciones existentes del proyecto de Microsoft.Common.props y Microsoft.Common.targets encuentran el archivo y lo importan.

Ámbito de búsqueda

Al buscar el archivo Directory.Build.props, MSBuild recorre la estructura del directorio hacia arriba desde la ubicación del proyecto $(MSBuildProjectFullPath), deteniéndose después de encontrar un archivo Directory.Build.props. Por ejemplo, si su $(MSBuildProjectFullPath) fuera c:\users\username\code\test\case1, MSBuild empezaría a buscar allí y, después, buscaría en la estructura del directorio hacia arriba hasta que encontrara un archivo Directory.Build.props, como en la siguiente estructura de directorio.

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

La ubicación del archivo de solución es irrelevante para Directory.Build.props.

Orden de importación

Directory.Build.props se importa de forma temprana en Microsoft.Common.props y las propiedades definidas posteriormente no están disponibles en el. Por tanto, evite hacer referencia a propiedades que todavía no estén definidas (y que se vayan a evaluar como vacías).

Las propiedades que se establecen en Directory.Build.props pueden reemplazarse en cualquier parte del archivo de proyecto o en archivos importados, por lo que debe pensar en la configuración de Directory.Build.props como la especificación de los valores predeterminados de los proyectos.

Directory.Build.targets se importa desde Microsoft.Common.targets después de importar los archivos .targets de paquetes NuGet. Por lo tanto, puede invalidar las propiedades y los destinos definidos en la mayor parte de la lógica de compilación, o bien, establecer las propiedades de todos los proyectos independientemente de lo que establezcan los proyectos individuales.

Cuando tenga que establecer una propiedad o definir un destino para un proyecto individual que invalide cualquier configuración anterior, coloque esa lógica en el archivo de proyecto después de la importación final. Para hacer esto en un proyecto de estilo SDK, primero debe reemplazar el atributo de estilo SDK por las importaciones equivalentes. Procedimiento para usar los SDK de proyecto de MSBuild.

Nota:

El motor de MSBuild lee todos los archivos importados durante la evaluación, antes de iniciar la ejecución de la compilación de cualquier proyecto (incluyendo cualquier PreBuildEvent), por lo que no se espera que estos archivos se modifiquen en PreBuildEvent ni en ninguna otra parte del proceso de compilación. Las modificaciones no surtirán efecto hasta la siguiente invocación de MSBuild.exe o la siguiente compilación de Visual Studio. Además, si el proceso de compilación contuviera muchas compilaciones de proyecto (como con proyectos dependientes de compatibilidad con múltiples versiones (multi-targeting) o de compilación), los archivos importados, incluyendo Directory.build.props, se leerán cuando se produzca la evaluación para cada compilación de proyecto individual.

Caso de uso: combinación de varios niveles

Imagínese que tiene esta estructura de solución estándar:

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

Puede que convenga tener propiedades comunes para todos los proyectos (1) , propiedades comunes para los proyectos src(2-src) y propiedades comunes para los proyectos test(2-test) .

Para que MSBuild combine correctamente los archivos "internos" (2-src y 2-test) con el archivo "externo" (1), debe tener en cuenta que, cuando MSBuild encuentra un archivo Directory.Build.props, detiene el análisis. Para continuar con el análisis y efectuar la combinación con el archivo externo, coloque este código en ambos archivos internos:

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

A continuación tiene un resumen del enfoque general de MSBuild:

  • Para un proyecto dado, MSBuild busca el primer Directory.Build.props hacia arriba en la estructura de la solución, lo combina con valores predeterminados y detiene el análisis.
  • Si desea que se busquen y combinen varios niveles, <Import...> (que se muestra anteriormente) el archivo "externo" desde el archivo "interno".
  • Si el archivo "externo" no importase también algo por encima de el, el análisis se detendrá en ese punto.

O más sencillo: el primer Directory.Build.props que no importa nada es donde se detiene MSBuild.

Para controlar el proceso de importación de forma más explícita, use las propiedades $(DirectoryBuildPropsPath), $(ImportDirectoryBuildProps), $(DirectoryBuildTargetsPath) y $(ImportDirectoryBuildTargets). La propiedad $(DirectoryBuildPropsPath) especifica la ruta de acceso al archivo Directory.Build.props que se va a usar; de forma similar, $(DirectoryBuildTargetsPath) especifica la ruta de acceso al archivo Directory.Build.targets.

Las propiedades booleanas $(ImportDirectoryBuildProps) y $(ImportDirectoryBuildTargets) se establecen en true de forma predeterminada, por lo que MSBuild normalmente busca estos archivos, pero puede establecerlas en false para impedir que MSBuild las importe.

Ejemplo

En este ejemplo se muestra el uso de la salida preprocesada para determinar dónde establecer una propiedad.

Para ayudarle a analizar el uso de una propiedad determinada que desea establecer, puede ejecutar MSBuild con el argumento /preprocess o /pp. El texto de salida es el resultado de todas las importaciones, incluidas las importaciones del sistema como Microsoft.Common.props que se importan implícitamente, y cualquiera de sus propias importaciones. Con esta salida, puede ver dónde debe establecerse la propiedad respecto al lugar en el que se usa su valor.

Por ejemplo, supongamos que tiene un proyecto de aplicación de consola simple de .NET Core o .NET 5 o posterior y desea personalizar la carpeta de salida intermedia, normalmente obj. La propiedad que especifica esta ruta de acceso es BaseIntermediateOutput. Si intentase colocar esto en un elemento PropertyGroup del archivo del proyecto junto con las otras propiedades que ya estén establecidas allí, como TargetFramework, al compilar el proyecto detectaría que la propiedad no surte efecto. Si ejecuta MSBuild con la opción /pp y busca la salida para BaseIntermediateOutputPath, puede ver por qué. En este caso, BaseIntermediateOutput se lee y se usa en Microsoft.Common.props.

Hay un comentario en Microsoft.Common.props que indica que la propiedad BaseIntermediateOutput debe establecerse aquí, antes de que otra propiedad, MSBuildProjectExtensionsPath, la use. También puede ver que, cuando BaseIntermediateOutputPath se establece inicialmente, hay una comprobación de un valor preexistente y, si no está definido, se establece en obj.

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

Por lo tanto, esta ubicación indica que para establecer esta propiedad se debe especificar en algún lugar antes de esto. Justo antes de este código en la salida preprocesada, se puede ver que Directory.Build.props se importa, por lo que se puede establecer BaseIntermediateOutputPath allí y se establecerá lo suficientemente pronto como para tener el efecto deseado.

La siguiente salida preprocesada abreviada muestra el resultado de colocar la configuración BaseIntermediateOutput en Directory.Build.props. Los comentarios de la parte superior de las importaciones estándar incluyen el nombre de archivo y, normalmente, cierta información útil sobre por qué se importa ese archivo.

<?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>
  ...