Personnaliser la build par dossier

Vous pouvez ajouter certains fichiers à importer par MSBuild pour remplacer les paramètres de propriétés par défaut et ajouter des cibles personnalisées. La portée de ces personnalisations peut être contrôlée au niveau du dossier où ces fichiers sont placés.

Cet article couvre les personnalisations applicables aux scénarios suivants :

  • Personnaliser les paramètres de génération pour de nombreux projets dans une solution
  • Personnaliser les paramètres de génération pour de nombreuses solutions sous un répertoire de fichiers commun
  • Personnaliser les paramètres de génération selon le sous-dossier d’une structure de dossiers complexe
  • Remplacer les paramètres par défaut, les dossiers de génération par défaut et d’autres comportements définis par un SDK comme Microsoft.Net.Sdk
  • Ajouter ou personnaliser des cibles de génération qui s’appliquent à n’importe quel nombre de projets ou de solutions

Si vous utilisez des projets C++, vous pouvez également utiliser les méthodes décrites dans Personnaliser des builds C++.

Directory.Build.props et Directory.Build.targets

Vous pouvez ajouter une nouvelle propriété à chaque projet en la définissant dans un seul fichier appelé Directory.Build.props au sein du dossier racine contenant votre source.

Quand MSBuild s’exécute, Microsoft.Common.props recherche le fichier Directory.Build.props dans votre structure de répertoire. S’il en trouve un, il l’importe et lit les propriétés définies dedans. Directory.Build.props est un fichier défini par l’utilisateur qui fournit des personnalisations aux projets situés dans un répertoire.

De même, Microsoft.Common.targets recherche Directory.Build.targets.

Directory.Build.props est importé au début de la séquence des fichiers importés, ce qui peut être important si vous devez définir une propriété utilisée par les importations, en particulier celles qui sont implicitement importées en utilisant l'attribut Sdk, comme lors de l'utilisation du SDK .NET dans la plupart des fichiers de projet .NET

Remarque

Les systèmes de fichiers Linux respectent la casse. Vérifiez que la casse du nom de fichier Directory.Build.props correspond exactement, sans quoi il ne sera pas détecté pendant le processus de build.

Pour plus d’informations, consultez ce problème GitHub.

Exemple avec Directory.Build.props

Par exemple, si vous souhaitez permettre à l’ensemble de vos projets d’accéder à la nouvelle fonctionnalité Roslyn /deterministic (qui est exposée dans la cible CoreCompile de Roslyn par la propriété $(Deterministic)), vous pouvez procéder comme suit.

  1. Créez un nouveau fichier à la racine de votre référentiel appelé Directory.Build.props.

  2. Ajoutez le code XML suivant au fichier.

    <Project>
     <PropertyGroup>
       <Deterministic>true</Deterministic>
     </PropertyGroup>
    </Project>
    
  3. Exécutez MSBuild. Les importations existantes de votre projet de Microsoft.Common.props et Microsoft.Common.targets trouvent le fichier et l’importent.

Étendue de la recherche

Quand vous recherchez un fichier Directory.Build.props, MSBuild remonte dans la structure des répertoires depuis l’emplacement de votre projet $(MSBuildProjectFullPath), et s’arrête après avoir localisé un fichier Directory.Build.props. Par exemple, si votre $(MSBuildProjectFullPath) était c:\users\username\code\test\case1, MSBuild commencerait à rechercher ici, puis remonterait dans la structure de répertoire jusqu’à ce qu’il trouve un fichier Directory.Build.props, comme dans la structure de répertoire suivante.

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

L’emplacement du fichier solution est sans importance pour Directory.Build.props.

Ordre d’importation

Directory.Build.props est importé très tôt dans Microsoft.Common.props et les propriétés définies ultérieurement ne sont pas disponibles pour ce dernier. Évitez donc de faire référence à des propriétés qui ne sont pas encore définies (et qui seront évaluées comme vides).

Les propriétés définies dans Directory.Build.props peuvent être remplacées ailleurs dans le fichier projet ou dans les fichiers importés. Vous devez donc considérer les paramètres indiqués dans Directory.Build.props comme spécifiant les valeurs par défaut de vos projets.

Directory.Build.targets est importé à partir de Microsoft.Common.targets après l’importation des fichiers .targets à partir des packages NuGet. Par conséquent, il peut remplacer les propriétés et les cibles spécifiées dans la majeure partie de la logique de build, ou définir des propriétés pour tous vos projets, indépendamment de ce qu’ils fixent à titre individuel.

Lorsque vous devez définir une propriété ou une cible qui remplace les paramètres précédents pour un projet individuel, placez cette logique dans le fichier projet après l’importation finale. Pour procéder de la sorte dans un projet de style kit SDK, vous devez d’abord remplacer l’attribut de style kit SDK par les importations équivalentes. Cf. Guide pratique pour utiliser les kits SDK de projet MSBuild.

Remarque

Le moteur MSBuild lit tous les fichiers importés pendant l’évaluation avant de commencer l’exécution de la build pour un projet (y compris les éventuels PreBuildEvent) : ces fichiers ne sont donc pas censés être modifiés par le PreBuildEvent ni par aucune autre partie du processus de build. Les modifications ne s’appliquent qu’après l’appel suivant de MSBuild.exe ou la build suivante de Visual Studio. Par ailleurs, si le processus de build contient de nombreuses builds de projet (comme avec le multiciblage ou la build de projets dépendants), les fichiers importés, y compris Directory.build.props, sont lus quand l’évaluation est effectuée pour la build de chaque projet individuel.

Cas d’utilisation : Fusion à plusieurs niveaux

Supposons que vous ayez la structure de solution standard suivante :

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

Il peut être souhaitable d’avoir des propriétés communes pour tous les projets (1), des propriétés communes pour les projets src(2-src) et des propriétés communes pour les projets test(2-test).

Pour que MSBuild fusionne correctement les fichiers « internes » (2-src et 2-test) avec le fichier « externe » (1), vous devez prendre en compte le fait qu’une fois que MSBuild a trouvé un fichier Directory.Build.props, il arrête l’analyse. Pour poursuivre l’analyse et fusionner les fichiers internes avec le fichier externe, placez ce code dans les deux fichiers internes :

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

Voici un résumé de l’approche générale MSBuild :

  • Pour un projet donné, MSBuild recherche le premier Directory.Build.props vers le haut dans la structure de la solution, le fusionne avec les valeurs par défaut et arrête la recherche.
  • Si vous souhaitez rechercher et fusionner plusieurs niveaux, effectuez une opération <Import...> (montrée précédemment) du fichier « externe » depuis le fichier « interne ».
  • Si le fichier « externe » n’importe pas non plus lui-même un élément de plus haut niveau, la recherche s’arrête là.

Ou plus simplement : MSBuild s’arrête au premier Directory.Build.props qui n’importe aucun élément.

Pour contrôler le processus d’importation de manière plus explicite, utilisez les propriétés $(DirectoryBuildPropsPath), $(ImportDirectoryBuildProps), $(DirectoryBuildTargetsPath) et $(ImportDirectoryBuildTargets). La propriété $(DirectoryBuildPropsPath) spécifie le chemin du fichier Directory.Build.props à utiliser. De même, $(DirectoryBuildTargetsPath) détermine le chemin du fichier Directory.Build.targets.

Les propriétés booléennes $(ImportDirectoryBuildProps) et $(ImportDirectoryBuildTargets) ont par défaut la valeur true. Par conséquent, MSBuild recherche en principe ces fichiers, mais vous pouvez les définir sur false pour empêcher MSBuild de les importer.

Exemple

Cet exemple montre l’utilisation de la sortie prétraitée pour déterminer où définir une propriété.

Pour vous aider à analyser l’utilisation d’une propriété particulière que vous souhaitez définir, vous pouvez exécuter MSBuild avec l’argument /preprocess ou /pp. Le texte de sortie est le résultat de toutes les importations, notamment des importations système comme Microsoft.Common.props qui sont implicitement importées, et de vos propres importations. Avec cette sortie, vous pouvez déterminer où votre propriété doit être définie selon l’emplacement où sa valeur est utilisée.

Imaginons par exemple que vous disposez d’un projet simple d’application console .NET Core ou .NET 5 ou version ultérieure et que vous souhaitez personnaliser le dossier de sortie intermédiaire, qui est normalement obj. La propriété qui spécifie ce chemin est BaseIntermediateOutput. Si vous essayez de placer cela dans un élément PropertyGroup de votre fichier projet avec les autres propriétés qui y sont déjà définies, comme TargetFramework, vous constaterez que la propriété n’a aucun effet quand vous générez le projet. Si vous exécutez MSBuild avec l’option /pp et recherchez la sortie de BaseIntermediateOutputPath, vous pouvez voir ce à quoi ce comportement est dû. Dans ce cas, BaseIntermediateOutput est lu et utilisé dans Microsoft.Common.props.

Un commentaire dans Microsoft.Common.props indique que la propriété BaseIntermediateOutput doit être définie ici avant d’être utilisée par une autre propriété MSBuildProjectExtensionsPath. Vous pouvez également constater que lorsque BaseIntermediateOutputPath est défini initialement, une vérification d’une valeur préexistante est effectuée. Si celle-ci n’est pas définie, elle est définie sur obj.

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

Ainsi, ce placement vous indique que pour définir cette propriété, vous devez la spécifier « plus tôt »que cela. Juste avant ce code dans la sortie prétraitée, vous pouvez voir que Directory.Build.props est importé : vous pouvez donc y définir BaseIntermediateOutputPath, ce qui lui permettra d’être définie suffisamment tôt pour avoir l’effet souhaité.

La sortie prétraitée abrégée suivante montre le résultat quand le paramètre BaseIntermediateOutput est placé dans Directory.Build.props. Les commentaires en haut des importations standard comprennent le nom de fichier et généralement des informations utiles indiquant pourquoi ce fichier est importé.

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