Comment MSBuild génère des projets

Comment MSBuild fonctionne-t-il réellement ? Dans cet article, vous allez découvrir comment MSBuild traite vos fichiers projet, qu’ils soient appelés à partir de Visual Studio, ou à partir d’une ligne de commande ou d’un script. Connaître le fonctionnement de MSBuild peut vous aider à mieux diagnostiquer les problèmes et à mieux personnaliser votre processus de génération. Cet article décrit le processus de génération et s’applique en grande partie à tous les types de projets.

Le processus de génération complet se compose du démarrage initial, de l’évaluation et de l’exécution des cibles et des tâches qui génèrent le projet. En plus de ces entrées, les importations externes définissent les détails du processus de génération, y compris les importations standard, telles que Microsoft.Common.targets et les importations configurables par l’utilisateur au niveau de la solution ou du projet.

Démarrage

MSBuild peut être appelé à partir de Visual Studio via le modèle d’objet MSBuild dans Microsoft.Build.dll, ou en appelant le fichier exécutable (MSBuild.exe ou dotnet build) directement sur la ligne de commande ou dans un script, comme dans les systèmes de CI. Dans tous les cas, les entrées qui affectent le processus de génération incluent le fichier projet (ou l’objet projet interne à Visual Studio), éventuellement un fichier de solution, des variables d’environnement et des commutateurs de ligne de commande ou leurs équivalents de modèle d’objet. Pendant la phase de démarrage, les options de ligne de commande ou les équivalents de modèle d’objet sont utilisés pour configurer les paramètres MSBuild, tels que la configuration des enregistreurs d’événements. Les propriétés définies sur la ligne de commande à l’aide du commutateur -property ou -p sont définies en tant que propriétés globales, qui remplacent toutes les valeurs qui seraient définies dans les fichiers projet, même si les fichiers projet sont lus ultérieurement.

Les sections suivantes concernent les fichiers d’entrée, tels que les fichiers de solution ou les fichiers projet.

Solutions et projets

Les instances MSBuild peuvent être constituées d’un seul projet ou de plusieurs projets dans le cadre d’une solution. Le fichier solution n’est pas un fichier XML MSBuild, mais MSBuild l’interprète pour connaître tous les projets qui doivent être générés pour les paramètres de configuration et de plateforme donnés. Lorsque MSBuild traite cette entrée XML, elle est appelée génération de solution. Elle comporte des points extensibles qui vous permettent d’exécuter quelque chose à chaque build de solution, mais étant donné que cette build est une exécution distincte des builds de projet individuels, aucun paramètre de propriétés ou définitions cibles de la build de solution n’est pertinent pour chaque build de projet.

Pour savoir comment étendre la génération de solution, consultez Personnaliser la génération de solution.

Générations Visual Studio et générations MSBuild.exe

Il existe des différences significatives entre le moment où les projets sont générés dans Visual Studio et lorsque vous appelez MSBuild directement, par le biais du fichier exécutable MSBuild ou lorsque vous utilisez le modèle d’objet MSBuild pour démarrer une génération. Visual Studio gère l’ordre de génération du projet pour les générations Visual Studio. Il appelle uniquement MSBuild au niveau du projet individuel, et dans ce cas, quelques propriétés booléennes (BuildingInsideVisualStudio, BuildProjectReferences) sont définies et affectent de manière significative les actions de MSBuild. À l’intérieur de chaque projet, l’exécution se produit de la même façon que lorsqu’elle est appelée via MSBuild, mais la différence se produit avec les projets référencés. Dans MSBuild, lorsque des projets référencés sont requis, une génération se produit réellement. Autrement dit, il exécute des tâches et des outils, et génère la sortie. Lorsqu’une génération Visual Studio trouve un projet référencé, MSBuild retourne uniquement les sorties attendues du projet référencé. Il permet à Visual Studio de contrôler la génération de ces autres projets. Visual Studio détermine l’ordre de génération et appelle MSBuild séparément (si nécessaire), le tout entièrement sous le contrôle de Visual Studio.

Une autre différence se produit lorsque MSBuild est appelé avec un fichier solution. MSBuild analyse le fichier solution, crée un fichier d’entrée XML standard, l’évalue et l’exécute en tant que projet. La génération de solution est exécutée avant tout projet. Lors de la génération à partir de Visual Studio, rien de cela ne se produit. MSBuild ne voit jamais le fichier solution. Par conséquent, la personnalisation de la génération de la solution (en utilisant before.SolutionName.sln.targets et after.SolutionName.sln.targets) ne s'applique qu'à MSBuild.exe, dotnet build, ou à la génération basée sur le modèle objet, et non à la génération de Visual Studio.

Kits SDK de projet

La fonctionnalité Kit de développement logiciel (SDK) pour les fichiers projet MSBuild est relativement nouvelle. Avant cette modification, les fichiers projet importaient explicitement les fichiers .targets et .props qui définissaient le processus de génération pour un type de projet particulier.

Les projets .NET Core importent la version du Kit de développement logiciel (SDK) .NET qui leur convient. Consultez la vue d’ensemble, Kits de développement logiciel (SDK) du projet .NET Core et la référence aux propriétés.

Phase d’évaluation

Cette section explique comment ces fichiers d’entrée sont traités et analysés pour produire des objets en mémoire qui déterminent ce qui sera généré.

L’objectif de la phase d’évaluation est de créer les structures d’objet en mémoire en fonction des fichiers XML d’entrée et de l’environnement local. La phase d’évaluation se compose de six passes qui traitent les fichiers d’entrée tels que les fichiers XML du projet, et les fichiers XML importés, généralement nommés fichiers .props ou .targets , selon qu’ils définissent principalement des propriétés ou définissent des cibles de génération. Chaque passe génère une partie des objets en mémoire qui sont ensuite utilisés dans la phase d’exécution pour générer les projets, mais aucune action de génération réelle ne se produit pendant la phase d’évaluation. Dans chaque passe, les éléments sont traités dans l’ordre dans lequel ils apparaissent.

Les passes de la phase d’évaluation sont les suivantes :

  • Évaluer les variables d’environnement
  • Évaluer les importations et les propriétés
  • Évaluer les définitions d’éléments
  • Évaluer les éléments
  • Évaluer les éléments UsingTask
  • Évaluer les cibles

Les importations et les propriétés sont évaluées dans la même passe en séquence d’apparence, comme si les importations sont développées en place. Par conséquent, les paramètres de propriété dans les fichiers précédemment importés sont disponibles dans les fichiers importés ultérieurement.

L’ordre de ces passes a des implications importantes et il est important de savoir quand personnaliser le fichier projet. Consultez Ordre d’évaluation des propriétés et des éléments.

Évaluer les variables d’environnement

Dans cette phase, les variables d’environnement sont utilisées pour définir des propriétés équivalentes. Par exemple, la variable d’environnement PATH est rendue disponible en tant que propriété $(PATH). Lorsqu’il est exécuté à partir de la ligne de commande ou d’un script, l’environnement de commande est utilisé normalement et, lorsqu’il est exécuté à partir de Visual Studio, l’environnement en vigueur au lancement de Visual Studio est utilisé.

Évaluer les importations et les propriétés

Au cours de cette phase, l’intégralité du XML d’entrée est lue, y compris les fichiers projet et toute la chaîne d’importations. MSBuild crée une structure XML en mémoire qui représente le XML du projet et tous les fichiers importés. À ce stade, les propriétés qui ne figurent pas dans les cibles sont évaluées et définies.

En conséquence de la lecture par MSBuild de tous les fichiers d’entrée XML au début de son processus, les modifications apportées à ces entrées pendant le processus de génération n’affectent pas la génération actuelle.

Les propriétés en dehors de n’importe quelle cible sont gérées différemment des propriétés au sein des cibles. Dans cette phase, seules les propriétés définies en dehors d’une cible sont évaluées.

Étant donné que les propriétés sont traitées dans l’ordre du passage des propriétés, une propriété à n’importe quel point de l’entrée peut accéder aux valeurs de propriété qui apparaissent plus tôt dans l’entrée, mais pas aux propriétés qui apparaissent ultérieurement.

Étant donné que les propriétés sont traitées avant l’évaluation des éléments, vous ne pouvez accéder à la valeur d’aucun élément pendant le passage des propriétés.

Évaluer les définitions d’éléments

Au cours de cette phase, les définitions d’éléments sont interprétées et une représentation en mémoire de ces définitions est créée.

Évaluer les éléments

Les éléments définis à l’intérieur d’une cible sont gérés différemment des éléments extérieurs à une cible. Au cours de cette phase, les éléments en dehors de toute cible, et leurs métadonnées associées, sont traités. Les métadonnées définies par les définitions d’éléments sont remplacées par les métadonnées définies sur les éléments. Étant donné que les éléments sont traités dans l’ordre dans lequel ils apparaissent, vous pouvez référencer des éléments qui ont été définis précédemment, mais pas ceux qui apparaissent plus tard. Étant donné que le passage d’éléments se fait après le passage des propriétés, les éléments peuvent accéder à n’importe quelle propriété si elles sont définies en dehors des cibles, que la définition de propriété apparaisse ultérieurement ou non.

Évaluer les éléments UsingTask

Au cours de cette phase, les éléments UsingTask sont lus et les tâches sont déclarées pour une utilisation ultérieure pendant la phase d’exécution.

Évaluer les cibles

Au cours de cette phase, toutes les structures d’objet cible sont créées en mémoire, en préparation de l’exécution. Aucune exécution réelle n’a lieu.

Phase d'exécution

Dans la phase d’exécution, les cibles sont ordonnées et exécutées, et toutes les tâches sont exécutées. Cependant, tout d’abord, les propriétés et les éléments qui sont définis dans des cibles sont évalués ensemble en une seule phase dans l’ordre dans lequel ils apparaissent. L’ordre de traitement est particulièrement différent de la façon dont les propriétés et les éléments qui ne sont pas dans une cible sont traités : toutes les propriétés d’abord, puis tous les éléments, dans des passes distinctes. Les modifications apportées aux propriétés et aux éléments d’une cible peuvent être observées après la cible où elles ont été modifiées.

Ordre de génération des cibles

Dans un seul projet, les cibles s’exécutent en série. La question centrale est de savoir comment déterminer l’ordre dans lequel tout générer afin que les dépendances soient utilisées pour générer les cibles dans le bon ordre.

L’ordre de génération cible est déterminé par l’utilisation des attributs BeforeTargets, DependsOnTargets et AfterTargets sur chaque cible. L’ordre des cibles ultérieures peut être influencé lors de l’exécution d’une cible antérieure si la cible précédente modifie une propriété référencée dans ces attributs.

Les règles de classement sont décrites dans Déterminer l’ordre de génération cible. Le processus est déterminé par une structure de pile contenant des cibles à générer. La cible située dans la partie supérieure de cette tâche démarre l’exécution et, si elle dépend de quoi que ce soit d’autre, ces cibles sont poussées vers le haut de la pile et commencent à s’exécuter. Lorsqu’il y a une cible sans dépendances, elle s’exécute jusqu’à l’achèvement et sa cible parente reprend.

Références de projets

Microsoft Build Engine peut prendre deux chemins de code : le chemin normal décrit ici et l’option de graphique décrite dans la section suivante.

Les projets individuels spécifient leur dépendance vis-à-vis d’autres projets par le biais d’éléments ProjectReference. Lorsqu’un projet situé en haut de la pile commence à être généré, il atteint le point où la cible ResolveProjectReferences exécute une cible standard définie dans les fichiers cibles courants.

ResolveProjectReferences appelle la tâche MSBuild avec les entrées des éléments ProjectReference pour obtenir les sorties. Les éléments ProjectReference sont transformés en éléments locaux tels que Reference. La phase d’exécution MSBuild pour le projet actuel s’interrompt pendant que la phase d’exécution commence à traiter le projet référencé (la phase d’évaluation est effectuée en premier selon les besoins). Le projet référencé est généré uniquement après avoir commencé à générer le projet dépendant, ce qui crée une arborescence de génération de projets.

Visual Studio permet de créer des dépendances de projets dans des fichiers solution (.sln). Les dépendances sont spécifiées dans le fichier solution et sont respectées uniquement lors de la génération d’une solution ou lors de la génération dans Visual Studio. Si vous générez un seul projet, ce type de dépendance est ignoré. Les références de solution sont transformées par MSBuild en éléments ProjectReference et sont ensuite traitées de la même manière.

Option Graph

Si vous spécifiez le commutateur de génération graphique (-graphBuild ou -graph), le ProjectReference devient un concept de première classe utilisé par MSBuild. MSBuild analyse tous les projets et construit le graphique d’ordre de génération, un graphique de dépendances réel des projets, qui est ensuite parcouru pour déterminer l’ordre de génération. Comme avec les cibles dans des projets individuels, MSBuild garantit que les projets référencés sont générés après les projets dont ils dépendent.

Exécution parallèle

Si vous utilisez la prise en charge du multiprocesseur (commutateur -maxCpuCount ou -m), MSBuild crée des nœuds, qui sont des processus MSBuild qui utilisent les noyaux de processeurs disponibles. Chaque projet est soumis à un nœud disponible. Au sein d’un nœud, les générations de projets individuelles s’exécutent en série.

Les tâches peuvent être activées pour l’exécution parallèle en définissant une variable booléenne BuildInParallel, qui est définie en fonction de la valeur de la propriété $(BuildInParallel) dans MSBuild. Pour les tâches qui sont activées pour l’exécution parallèle, un planificateur de travail gère les nœuds et affecte le travail aux nœuds.

Consultez Génération parallèle de plusieurs projets avec MSBuild

Importations standard

Microsoft.Common.props et Microsoft.Common.targets sont tous deux importés par des fichiers projet .NET (explicitement ou implicitement dans des projets de style Kit de développement logiciel (SDK)) et se trouvent dans le dossier MSBuild\Current\bin d’une installation Visual Studio. Les projets C++ ont leur propre hiérarchie d’importations. Consultez Composants internes MSBuild pour les projets C++.

Le fichier Microsoft.Common.props définit les valeurs par défaut que vous pouvez remplacer. Il est importé (explicitement ou implicitement) au début d’un fichier projet. De cette façon, les paramètres de votre projet apparaissent après les valeurs par défaut, afin qu’ils les remplacent.

Le fichier Microsoft.Common.targets et les fichiers cibles qu’il importe définissent le processus de génération standard pour les projets .NET. Il fournit également des points d’extension que vous pouvez utiliser pour personnaliser la génération.

Dans l’implémentation, Microsoft.Common.targets est un wrapper fin qui importe Microsoft.Common.CurrentVersion.targets. Ce fichier contient les paramètres des propriétés standard et définit les cibles réelles qui définissent le processus de génération. La cible Build est définie ici, mais elle est en fait elle-même vide. Toutefois, la cible Build contient l’attribut DependsOnTargets qui spécifie les cibles individuelles qui composent les étapes de génération réelles, à savoir BeforeBuild, CoreBuild et AfterBuild. La cible Build est définie comme suit :

  <PropertyGroup>
    <BuildDependsOn>
      BeforeBuild;
      CoreBuild;
      AfterBuild
    </BuildDependsOn>
  </PropertyGroup>

  <Target
      Name="Build"
      Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
      DependsOnTargets="$(BuildDependsOn)"
      Returns="@(TargetPathWithTargetPlatformMoniker)" />

BeforeBuild et AfterBuild sont des points d’extension. Ils sont vides dans le fichier Microsoft.Common.CurrentVersion.targets, mais les projets peuvent fournir leurs propres cibles BeforeBuild et AfterBuild avec des tâches qui doivent être effectuées avant ou après le processus de génération principal. AfterBuild est exécuté avant la cible non opérationnelle, Build, car AfterBuild apparaît dans l’attribut DependsOnTargets sur la cible Build, mais il se produit après CoreBuild.

La cible CoreBuild contient les appels aux outils de génération, comme suit :

  <PropertyGroup>
    <CoreBuildDependsOn>
      BuildOnlySettings;
      PrepareForBuild;
      PreBuildEvent;
      ResolveReferences;
      PrepareResources;
      ResolveKeySource;
      Compile;
      ExportWindowsMDFile;
      UnmanagedUnregistration;
      GenerateSerializationAssemblies;
      CreateSatelliteAssemblies;
      GenerateManifests;
      GetTargetPath;
      PrepareForRun;
      UnmanagedRegistration;
      IncrementalClean;
      PostBuildEvent
    </CoreBuildDependsOn>
  </PropertyGroup>
  <Target
      Name="CoreBuild"
      DependsOnTargets="$(CoreBuildDependsOn)">

    <OnError ExecuteTargets="_TimeStampAfterCompile;PostBuildEvent" Condition="'$(RunPostBuildEvent)'=='Always' or '$(RunPostBuildEvent)'=='OnOutputUpdated'"/>
    <OnError ExecuteTargets="_CleanRecordFileWrites"/>

  </Target>

Le tableau suivant décrit ces cibles : certaines cibles s’appliquent uniquement à certains types de projets.

Cible Description
BuildOnlySettings Paramètres pour les générations réelles uniquement, pas pour quand MSBuild est appelé lors du chargement du projet par Visual Studio.
PrepareForBuild Préparer les conditions préalables à la génération
PreBuildEvent Point d’extension pour les projets afin de définir les tâches à exécuter avant la génération
ResolveProjectReferences Analyser les dépendances de projet et générer des projets référencés
ResolveAssemblyReferences Localisez les assemblies référencés.
ResolveReferences Se compose de ResolveProjectReferences et ResolveAssemblyReferences pour rechercher toutes les dépendances
PrepareResources Traiter les fichiers de ressources
ResolveKeySource Résolvez la clé de nom fort utilisée pour signer l’assembly et le certificat utilisé pour signer les manifestes ClickOnce.
Compiler Appeler le compilateur
ExportWindowsMDFile Générez un fichier WinMD à partir des fichiers WinMDModule générés par le compilateur.
UnmanagedUnregistration Supprimer/nettoyer les entrées de registre COM Interop d’une génération précédente
GenerateSerializationAssemblies Générez un assembly de sérialisation XML à l’aide de sgen.exe.
CreateSatelliteAssemblies Créez un assembly satellite pour chaque culture unique dans les ressources.
Générer des manifestes Génère des manifestes de déploiement et d’application ClickOnce ou un manifeste natif.
GetTargetPath Retourne un élément contenant le produit de la génération (exécutable ou assembly) pour ce projet, avec des métadonnées.
PrepareForRun Copiez les sorties de génération dans le répertoire final si elles ont été modifiées.
UnmanagedRegistration Définir des entrées de registre pour COM Interop
IncrementalClean Supprimez les fichiers qui ont été produits dans une génération précédente, mais qui ne l’ont pas été dans la génération actuelle. Cela est nécessaire pour que Clean fonctionne dans les générations incrémentielles.
PostBuildEvent Point d’extension pour les projets afin de définir les tâches à exécuter après la génération

La plupart des cibles du tableau précédent se trouvent dans des importations spécifiques au langage, telles que Microsoft.CSharp.targets. Ce fichier définit les étapes du processus de génération standard spécifique aux projets C# .NET. Par exemple, il contient la cible Compile qui appelle réellement le compilateur C#.

Importations configurables par l’utilisateur

Outre les importations standard, vous pouvez ajouter plusieurs importations pour personnaliser le processus de génération.

  • Directory.Build.props
  • Directory.Build.targets

Ces fichiers sont lus par les importations standard pour tous les projets dans n’importe quel sous-dossier auxquels ils sont rattachés. Ceci est commun au niveau de la solution pour les paramètres permettant de contrôler tous les projets de la solution, mais peut également se produire plus haut dans le système de fichiers, jusqu’à la racine du lecteur.

Le fichier Directory.Build.props étant importé par Microsoft.Common.props, le propriétés qui y sont définies sont disponibles dans le fichier projet. Elles peuvent être redéfinies dans le fichier projet pour personnaliser les valeurs par projet. Le fichier Directory.Build.targets est lu après le fichier projet. Il contient généralement des cibles, mais ici vous pouvez également définir des propriétés que vous ne souhaitez pas que des projets individuels redéfinissent.

Personnalisations dans un fichier projet

Visual Studio met à jour vos fichiers projet à mesure que vous apportez des modifications dans l’Explorateur de solutions, la fenêtre Propriétés ou dans Propriétés du projet, mais vous pouvez également apporter vos propres modifications en modifiant directement le fichier projet.

De nombreux comportements de génération peuvent être configurés en définissant les propriétés MSBuild, soit dans le fichier projet pour les paramètres locaux d’un projet, soit comme mentionné dans la section précédente, en créant un fichier Directory.Build.props pour définir des propriétés globalement pour des dossiers entiers de projets et de solutions. Pour les générations ad hoc sur la ligne de commande, ou les scripts, vous pouvez également utiliser l’option /p sur la ligne de commande pour définir les propriétés d’un appel particulier de MSBuild. Pour plus d’informations sur les propriétés que vous pouvez définir, consultez Propriétés communes du projet MSBuild.