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 trabaja con proyectos C++, también puede utilizar los métodos descritos en Personalizar compilaciones 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, este es un archivo Directory.Build.props que establece el directorio de salida para todos los proyectos de una solución de Visual Studio. La salida de cada proyecto se coloca bajo su propio nombre de proyecto. En este ejemplo, el archivo Directory.Build.props se encuentra en una carpeta de solución, con muchos proyectos en subcarpetas debajo. La propiedad $(MSBuildProjectName)
proporciona el nombre de cada proyecto. Dado que el archivo Directory.Build.props se importa en cada proyecto durante su propia compilación, se evalúa como el valor adecuado para cada proyecto individual de la solución.
Limpie la solución para quitar los archivos de salida antiguos.
msbuild /t:Clean SolutionName.sln
Crear un archivo en la raíz de su repositorio denominado Directory.Build.props.
Agregar el siguiente XML al archivo.
<Project> <PropertyGroup> <OutDir>C:\output\$(MSBuildProjectName)</OutDir> </PropertyGroup> </Project>
Nota:
La propiedad
$(OutDir)
es una ruta de acceso absoluta a la salida y su uso omite la creación de subcarpetas para la configuración, la plataforma de destino o el entorno de ejecución que se usan normalmente en proyectos de .NET. Intente usar la propiedadBaseOutputPath
en su lugar si desea crear las subcarpetas habituales en una ruta de acceso de salida personalizada.Ejecutar MSBuild. Las importaciones existentes del proyecto de Microsoft.Common.props y Microsoft.Common.targets buscan el archivo Directory.Build.props y lo importan, y la nueva carpeta de salida se usa para todos los proyectos de esa carpeta.
Á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>
...