Option --output au niveau de la solution non valide pour les commandes liées à la build

Dans le kit de développement logiciel (SDK) 7.0.200, un changement a été apporté pour ne plus accepter l’option --output/-o lorsqu’un fichier solution est utilisé avec les commandes suivantes :

  • build
  • clean
  • pack
  • publish
  • store
  • test
  • vstest

En effet, la sémantique de la propriété OutputPath, contrôlée par l’option --output/-o, n’est pas bien définie pour les solutions. Les projets ainsi créés voient leur sortie placée dans le même répertoire, ce qui est incohérent et a entraîné un certain nombre de problèmes signalés par l’utilisateur.

Ce changement a été réduit à une gravité du niveau d’avertissement dans le kit SDK 7.0.201. Par ailleurs, pack a été supprimé de la liste des commandes concernées.

Version introduite

Kit SDK .NET 7.0.200, réduction à un avertissement dans le kit SDK 7.0.201 uniquement.

Comportement précédent

Auparavant, si vous indiquiez --output/-o lorsque vous utilisiez un fichier solution, la sortie de tous les projets générés était placée dans le répertoire spécifié dans un ordre non défini et incohérent.

Nouveau comportement

L’interface CLI dotnet produit une erreur si l’option --output/-o est utilisée avec un fichier solution. À partir du kit SDK 7.0.201, un avertissement est émis à la place. Dans le cas de dotnet pack, aucune erreur ni aucun avertissement n’est généré.

Type de changement cassant

Ce changement cassant peut exiger des modifications pour générer des scripts et des pipelines d’intégration continue. Par conséquent, il affecte à la fois la compatibilité binaire et la compatibilité source.

Raison du changement

Ce changement a été apporté parce que la sémantique de la propriété OutputPath, contrôlée par l’option --output/-o, n’est pas bien définie pour les solutions. Les projets ainsi créés voient leur sortie placée dans le même répertoire, ce qui est incohérent et a entraîné un certain nombre de problèmes signalés par l’utilisateur.

Lorsqu’une solution est générée avec l’option --output, la propriété OutputPath est définie sur la même valeur pour tous les projets, ce qui signifie que la sortie de tous les projets est placée dans le même répertoire. Selon la complexité des projets de la solution, des résultats différents et incohérents peuvent être observés. Examinons quelques exemples de formes de solution différentes et voyons comment elles sont affectées par un OutputPath partagé.

Projet unique avec TargetFramework unique

Prenons une solution contenant un seul projet qui cible un seul TargetFramework, net7.0. Dans ce cas, fournir l’option --output équivaut à définir la propriété OutputPath dans le fichier projet. Au cours d’une build (ou d’autres commandes, mais nous allons pour l’instant limiter la discussion à la build), toutes les sorties du projet sont placées dans le répertoire spécifié.

Projet unique avec plusieurs TargetFramework

Prenons maintenant une solution contenant un seul projet avec plusieurs TargetFrameworks, net6.0 et net7.0. En raison du multi-ciblage, le projet est généré deux fois, une pour chaque TargetFramework. Pour chacune de ces builds « internes », OutputPath est défini sur la même valeur. La sortie de chacune des builds internes est donc placée dans le même répertoire. Cela signifie que la dernière build terminée remplace les sorties de l’autre. Or, dans un système de build parallèle comme le fonctionnement par défaut MSBuild, la « dernière » est indéterminée.

Bibliothèque => console => test avec TargetFramework unique

Prenons maintenant une solution contenant un projet de bibliothèque, un projet de console qui fait référence au projet de bibliothèque et un projet de test qui fait référence au projet de console. Tous ces projets ciblent un seul TargetFramework, net7.0. Dans ce cas, le projet de bibliothèque est généré en premier, suivi du projet de console. Le projet de test, généré en dernier, fait référence au projet de console. Pour chaque projet généré, la sortie de chaque build est copiée dans le répertoire spécifié par OutputPath. Le répertoire final contient donc les ressources des trois projets. Cela fonctionne pour les tests. En publication toutefois, des ressources de test risquent d’être envoyées en production.

Bibliothèque => console => test avec plusieurs TargetFramework

Prenons maintenant la même chaîne de projets et ajoutons une build net6.0TargetFramework en plus de la build net7.0. Le même problème que la build multi-cible à un seul projet se produit : la copie des ressources propres au TFM dans le répertoire spécifié est incohérente.

Applications multiples

Jusqu’à présent, nous avons examiné des scénarios présentant un graphique de dépendance linéaire, mais de nombreuses solutions peuvent contenir plusieurs applications associées. Plusieurs applications sont alors générées simultanément dans le même dossier de sortie. Si ces applications incluent un fichier de dépendances portant le même nom, la build peut échouer par intermittence lorsque plusieurs projets tentent d’écrire simultanément dans ce fichier dans le chemin de sortie.

Si plusieurs applications dépendent de différentes versions d’un fichier, la version du fichier copiée dans le chemin de sortie peut, même si la build réussit, se révéler non déterministe. Cela peut se produire lorsque les projets dépendent (éventuellement transitivement) de différentes versions d’un package NuGet. Au sein d’un seul projet, NuGet permet de s’assurer que ses dépendances (y compris toutes les dépendances transitives par le biais de packages NuGet ou de références de projet) sont unifiées sur la même version. Étant donné que cette unification s’effectue dans le contexte d’un seul projet et des projets dépendants, il est possible de résoudre différentes versions d’un package lorsque deux projets de niveau supérieur distincts sont générés. Si le projet qui dépend de la version supérieure copie la dépendance en dernier, les applications s’exécutent souvent correctement. Si en revanche c’est la version inférieure qui est copiée en dernier, l’application compilée sur la version supérieure ne parvient pas à charger l’assembly à l’exécution. Étant donné que la version copiée peut se révéler non déterministe, on obtient potentiellement des builds sporadiques et peu fiables dont il est très difficile de diagnostiquer le problème.

Autres exemples

Pour obtenir d’autres exemples de la façon dont cette erreur sous-jacente se présente dans la pratique, consultez la discussion sur dotnet/sdk#15607.

La recommandation générale consiste à effectuer l’action précédemment lancée sans l’option --output/-o, puis à déplacer la sortie dans l’emplacement souhaité une fois la commande terminée. Il est également possible de procéder à l’action sur un projet spécifique tout en appliquant l’option --output/-o, car elle présente une sémantique mieux définie.

Si vous souhaitez maintenir exactement le comportement existant, vous pouvez utiliser l’indicateur --property pour définir une propriété MSBuild sur le répertoire souhaité. La propriété à utiliser varie en fonction de la commande :

Commande Propriété Exemple
build OutputPath dotnet build --property:OutputPath=DESIRED_PATH
clean OutputPath dotnet clean --property:OutputPath=DESIRED_PATH
pack PackageOutputPath dotnet pack --property:PackageOutputPath=DESIRED_PATH
publish PublishDir dotnet publish --property:PublishDir=DESIRED_PATH
store OutputPath dotnet store --property:OutputPath=DESIRED_PATH
test TestResultsDirectory dotnet test --property:OutputPath=DESIRED_PATH

REMARQUE : pour de meilleurs résultats, DESIRED_PATH doit être un chemin d’accès absolu. Les chemins relatifs sont « ancrés » (c’est-à-dire rendus absolus) d’une manière potentiellement inattendue. Ils peuvent ne pas fonctionner de la même façon avec toutes les commandes.