Quelle est la meilleure pratique pour «Copy Local» et avec des références de projet?

154

J'ai un gros fichier de solution c # (~ 100 projets) et j'essaie d'améliorer les temps de construction. Je pense que "Copy Local" est un gaspillage dans de nombreux cas pour nous, mais je m'interroge sur les meilleures pratiques.

Dans notre .sln, nous avons l'application A dépendant de l'assemblage B qui dépend de l'assemblage C. Dans notre cas, il y a des dizaines de "B" et une poignée de "C". Comme ils sont tous inclus dans le .sln, nous utilisons des références de projet. Tous les assemblys sont actuellement construits dans $ (SolutionDir) / Debug (ou Release).

Par défaut, Visual Studio marque ces références de projet comme «Copier local», ce qui entraîne la copie de chaque «C» dans $ (SolutionDir) / Debug une fois pour chaque «B» généré. Cela semble inutile. Que peut-il se passer si je désactive simplement "Copier local"? Que font les autres personnes disposant de grands systèmes?

SUIVRE:

De nombreuses réponses suggèrent de diviser la construction en fichiers .sln plus petits ... Dans l'exemple ci-dessus, je construirais d'abord les classes de base "C", suivies de la plupart des modules "B", puis de quelques applications, " UNE". Dans ce modèle, j'ai besoin d'avoir des références non-projet à C à partir de B. Le problème que je rencontre là-bas est que "Debug" ou "Release" est intégré dans le chemin d'indication et je finis par construire mes versions Release de "B" contre les versions de débogage de "C".

Pour ceux d'entre vous qui ont divisé l'accumulation en plusieurs fichiers .sln, comment gérez-vous ce problème?

Dave Moore
la source
9
Vous pouvez faire référence à votre chemin d'indication dans le répertoire Debug ou Release en éditant directement le fichier projet. Utilisez $ (Configuration) à la place de Debug ou Release. Par exemple, <HintPath> .. \ output \ $ (Configuration) \ test.dll </HintPath> C'est pénible lorsque vous avez beaucoup de références (même si cela ne devrait pas être difficile pour quelqu'un d'écrire un complément sur gérer cela).
ultravelocity
4
«Copier local» dans Visual Studio est-il le même que <Private>True</Private>dans un csproj?
Colonel Panic
Mais diviser a .slnen plus petits rompt le calcul d'auto-dépendance de VS de <ProjectReference/>s. Je suis passé de plusieurs plus petits .slnà un seul gros .slnmoi-même juste parce que VS cause moins de problèmes de cette façon… Alors, peut-être que le suivi suppose une solution pas nécessairement la meilleure à la question originale? ;-)
binki
Juste par curiosité. Pourquoi compliquer les choses et avoir plus de 100 projets en premier lieu? Est-ce une mauvaise conception ou quoi?
utileBee
@ColonelPanic Oui. Au moins, c'est la chose qui change sur le disque lorsque je change cette bascule dans l'interface graphique.
Zero3

Réponses:

85

Dans un projet précédent, j'ai travaillé avec une grande solution avec des références de projet et j'ai également rencontré un problème de performance. La solution était triple:

  1. Définissez toujours la propriété Copy Local sur false et appliquez-la via une étape msbuild personnalisée

  2. Définissez le répertoire de sortie de chaque projet sur le même répertoire (de préférence par rapport à $ (SolutionDir)

  3. Les cibles cs par défaut livrées avec le framework calculent l'ensemble de références à copier dans le répertoire de sortie du projet en cours de construction. Comme cela nécessite de calculer une fermeture transitive sous la relation «Références», cela peut devenir TRÈS coûteux. Ma solution de contournement pour cela était de redéfinir la GetCopyToOutputDirectoryItemscible dans un fichier de cibles commun (par exemple Common.targets) importé dans chaque projet après l'importation du fichier Microsoft.CSharp.targets. Résultat: chaque fichier de projet ressemble à ce qui suit:

    <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <PropertyGroup>
        ... snip ...
      </ItemGroup>
      <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
      <Import Project="[relative path to Common.targets]" />
      <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
           Other similar extension points exist, see Microsoft.Common.targets.
      <Target Name="BeforeBuild">
      </Target>
      <Target Name="AfterBuild">
      </Target>
      -->
    </Project>

Cela a réduit notre temps de construction à un moment donné de quelques heures (principalement en raison de contraintes de mémoire) à quelques minutes.

Le redéfini GetCopyToOutputDirectoryItemspeut être créé en copiant les lignes 2,438–2,450 et 2,474–2,524 de C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targetsdans Common.targets.

Par souci d'exhaustivité, la définition cible résultante devient alors:

<!-- This is a modified version of the Microsoft.Common.targets
     version of this target it does not include transitively
     referenced projects. Since this leads to enormous memory
     consumption and is not needed since we use the single
     output directory strategy.
============================================================
                    GetCopyToOutputDirectoryItems

Get all project items that may need to be transferred to the
output directory.
============================================================ -->
<Target
    Name="GetCopyToOutputDirectoryItems"
    Outputs="@(AllItemsFullPathWithTargetPath)"
    DependsOnTargets="AssignTargetPaths;_SplitProjectReferencesByFileExistence">

    <!-- Get items from this project last so that they will be copied last. -->
    <CreateItem
        Include="@(ContentWithTargetPath->'%(FullPath)')"
        Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"
            >
        <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
    </CreateItem>

    <CreateItem
        Include="@(_EmbeddedResourceWithTargetPath->'%(FullPath)')"
        Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"
            >
        <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
    </CreateItem>

    <CreateItem
        Include="@(Compile->'%(FullPath)')"
        Condition="'%(Compile.CopyToOutputDirectory)'=='Always' or '%(Compile.CopyToOutputDirectory)'=='PreserveNewest'">
        <Output TaskParameter="Include" ItemName="_CompileItemsToCopy"/>
    </CreateItem>
    <AssignTargetPath Files="@(_CompileItemsToCopy)" RootFolder="$(MSBuildProjectDirectory)">
        <Output TaskParameter="AssignedFiles" ItemName="_CompileItemsToCopyWithTargetPath" />
    </AssignTargetPath>
    <CreateItem Include="@(_CompileItemsToCopyWithTargetPath)">
        <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
    </CreateItem>

    <CreateItem
        Include="@(_NoneWithTargetPath->'%(FullPath)')"
        Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"
            >
        <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
        <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
    </CreateItem>
</Target>

Avec cette solution de contournement en place, j'ai trouvé qu'il était possible d'avoir jusqu'à> 120 projets dans une seule solution, cela présente le principal avantage que l'ordre de construction des projets peut toujours être déterminé par VS au lieu de le faire à la main en fractionnant votre solution .

Bas Bossink
la source
Pouvez-vous décrire les changements que vous avez apportés et pourquoi? Mes globes oculaires sont trop fatigués après une longue journée de codage pour essayer de faire de l'ingénierie inverse moi-même :)
Charlie Flowers
Que diriez-vous d'essayer de copier et coller à nouveau - SO foiré comme 99% des balises.
ZXX
@Charlie Flowers, @ZXX a modifié le texte pour en faire une description n'a pas pu mettre le XML en page correctement.
Bas Bossink
1
À partir de Microsoft.Common.targets: GetCopyToOutputDirectoryItems Obtenez tous les éléments de projet qui peuvent avoir besoin d'être transférés vers le répertoire de sortie. Cela comprend les bagages provenant de projets référencés de manière transitoire. Il semblerait que cette cible calcule la fermeture transitive complète des éléments de contenu pour tous les projets référencés; Cependant, ce n'est pas le cas.
Brans Ds
2
Il collecte uniquement les éléments de contenu de ses enfants immédiats et non des enfants d'enfants. La raison pour laquelle cela se produit est que la liste ProjectReferenceWithConfiguration qui est consommée par _SplitProjectReferencesByFileExistence est uniquement remplie dans le projet actuel et est vide dans les enfants. La liste vide fait que _MSBuildProjectReferenceExistent est vide et met fin à la récursivité. Cela ne semble donc pas utile.
Brans Ds
32

Je vous suggère de lire les articles de Patric Smacchia sur ce sujet:

Les projets CC.Net VS reposent sur l'option d'assembly de référence locale de copie définie sur true. [...] Non seulement cela augmente considérablement le temps de compilation (x3 dans le cas de NUnit), mais aussi ça gâche votre environnement de travail. Dernier point mais non le moindre, cela présente le risque de problèmes potentiels de versioning. Btw, NDepend émettra un avertissement s'il trouve 2 assemblys dans 2 répertoires différents avec le même nom, mais pas le même contenu ou la même version.

La bonne chose à faire est de définir 2 répertoires $ RootDir $ \ bin \ Debug et $ RootDir $ \ bin \ Release, et de configurer vos projets VisualStudio pour émettre des assemblys dans ces répertoires. Toutes les références de projet doivent référencer des assemblys dans le répertoire Debug.

Vous pouvez également lire cet article pour vous aider à réduire le nombre de vos projets et à améliorer votre temps de compilation.

Julien Hoarau
la source
1
J'aimerais pouvoir recommander les pratiques de Smacchia avec plus d'un vote favorable! Réduire le nombre de projets est essentiel, et non diviser la solution.
Anthony Mastrean
23

Je suggère d'avoir une copie locale = false pour presque tous les projets sauf celui qui se trouve en haut de l'arborescence des dépendances. Et pour toutes les références de celle du haut, copiez local = true. Je vois de nombreuses personnes suggérant de partager un répertoire de sortie; Je pense que c'est une idée horrible basée sur l'expérience. Si votre projet de démarrage contient des références à une dll que tout autre projet contient une référence à vous, vous rencontrerez à un moment donné une violation d'accès \ partage même si copier local = false sur tout et votre build échouera. Ce problème est très ennuyeux et difficile à retracer. Je suggère complètement de rester à l'écart d'un répertoire de sortie de partition et au lieu d'avoir le projet en haut de la chaîne de dépendances, écrivez les assemblys nécessaires dans le dossier correspondant. Si vous n'avez pas de projet en haut, alors je suggérerais une copie post-build pour que tout soit au bon endroit. Aussi, j'essaierais de garder à l'esprit la facilité de débogage. Tous les projets exe que je laisse toujours copier local = true pour que l'expérience de débogage F5 fonctionne.

Aaron Stainback
la source
2
J'avais cette même idée et j'espérais trouver quelqu'un d'autre qui pensait la même chose ici; cependant, je suis curieux de savoir pourquoi cet article n'a pas plus de votes positifs. Les gens qui ne sont pas d'accord: pourquoi n'êtes-vous pas d'accord?
bwerks
Non, cela ne peut pas arriver à moins que le même projet ne soit construit deux fois, pourquoi serait-il écrasé par une violation \ access \ sharing s'il est construit une fois et ne copie aucun fichier?
paulm
Ce. Si le flux de travail de développement nécessite la construction d'un projet du sln, alors qu'un autre projet exécutable de la solution est en cours d'exécution, tout avoir dans le même répertoire de sortie sera un désordre. Il est préférable de séparer les dossiers de sortie exécutables dans ce cas.
Martin Ba
10

Vous avez raison. CopyLocal tuera absolument vos temps de construction. Si vous avez une grande arborescence source, vous devez désactiver CopyLocal. Malheureusement, il n'est pas aussi facile qu'il devrait l'être de le désactiver proprement. J'ai répondu à cette question exacte sur la désactivation de CopyLocal à Comment remplacer le paramètre CopyLocal (Private) pour les références dans .NET de MSBUILD . Vérifiez-le. Ainsi que les meilleures pratiques pour les grandes solutions dans Visual Studio (2008).

Voici quelques informations supplémentaires sur CopyLocal tel que je le vois.

CopyLocal a été implémenté vraiment pour prendre en charge le débogage local. Lorsque vous préparez votre application pour l'empaquetage et le déploiement, vous devez créer vos projets dans le même dossier de sortie et vous assurer que vous disposez de toutes les références dont vous avez besoin.

J'ai écrit sur la façon de gérer la création de grandes arborescences sources dans l'article MSBuild: Meilleures pratiques pour la création de builds fiables, partie 2 .

Sayed Ibrahim Hashimi
la source
8

À mon avis, avoir une solution avec 100 projets est une GROSSE erreur. Vous pourriez probablement diviser votre solution en petites unités logiques valides, simplifiant ainsi la maintenance et les builds.

Bruno Shine
la source
1
Bruno, s'il vous plaît voir ma question de suivi ci-dessus - si nous divisons en fichiers .sln plus petits, comment gérez-vous l'aspect Debug vs Release qui est ensuite intégré dans le chemin d'indication de mes références?
Dave Moore
1
Je suis d'accord avec ce point, la solution avec laquelle je travaille a ~ 100 projets, dont seulement une poignée a plus de 3 classes, les temps de construction sont choquants, et par conséquent mes prédécesseurs ont divisé la solution en 3 qui casse complètement 'find toutes les références et refactoring. Le tout pourrait tenir dans une poignée de projets qui se construiraient en quelques secondes!
Jon M
Dave, bonne question. Là où je travaille, nous avons des scripts de construction qui font des choses comme construire des dépendances pour une solution donnée et placer les binaires quelque part où la solution en question peut les obtenir. Ces scripts sont paramétrés pour les versions de débogage et de publication. L'inconvénient est un temps supplémentaire à l'avance pour créer lesdits scripts, mais ils peuvent être réutilisés dans les applications. Cette solution a bien fonctionné selon mes normes.
jyoungdev
7

Je suis surpris que personne n'ait mentionné l'utilisation de liens physiques. Au lieu de copier les fichiers, il crée un lien direct vers le fichier d'origine. Cela économise de l'espace disque et accélère considérablement la construction. Cela peut être activé sur la ligne de commande avec les propriétés suivantes:

/ p: CreateHardLinksForAdditionalFilesIfPossible = true; CreateHardLinksForCopyAdditionalFilesIfPossible = true; CreateHardLinksForCopyFilesToOutputDirectoryIfPossible = true; CreateHardLinksForCopyLocalIfPossible = true; CreateHardLinksForPossibleIfPossible = true; CreateHardLinksFor

Vous pouvez également l'ajouter à un fichier d'importation central afin que tous vos projets puissent également bénéficier de cet avantage.

Matt Slagle
la source
5

Si vous avez défini la structure de dépendance via des références de projet ou via des dépendances au niveau de la solution, il est prudent d'activer "Copier local", je dirais même que c'est une bonne pratique de le faire, car cela vous permettra d'utiliser MSBuild 3.5 pour exécuter votre build en parallèle ( via / maxcpucount) sans que différents processus ne se déclenchent l'un sur l'autre lors de la tentative de copie d'assemblys référencés.

Torbjörn Gyllebring
la source
4

notre «meilleure pratique» est d'éviter les solutions avec de nombreux projets. Nous avons un répertoire nommé "matrix" avec les versions actuelles des assemblys, et toutes les références proviennent de ce répertoire. Si vous modifiez un projet et que vous pouvez dire "maintenant le changement est terminé", vous pouvez copier l'assemblage dans le répertoire "matrice". Ainsi, tous les projets qui dépendent de cet assemblage auront la version actuelle (= la plus récente).

Si vous avez peu de projets en solution, le processus de construction est beaucoup plus rapide.

Vous pouvez automatiser l'étape "Copier l'assemblage dans le répertoire de la matrice" en utilisant les macros de Visual Studio ou avec "menu -> outils -> outils externes ...".

TcKs
la source
3

Vous n'avez pas besoin de modifier les valeurs CopyLocal. Tout ce que vous avez à faire est de prédéfinir un $ (OutputPath) commun pour tous les projets de la solution et de prédéfinir $ (UseCommonOutputDirectory) sur true. Voir ceci: http://blogs.msdn.com/b/kirillosenkov/archive/2015/04/04/using-a-common-intermediate-and-output-directory-for-your-solution.aspx

Éclat
la source
Je ne suis pas sûr que cela était disponible en 2009, mais cela semble bien fonctionner en 2015. Merci!
Dave Moore
2

Définir CopyLocal = false réduira le temps de génération, mais peut entraîner différents problèmes lors du déploiement.

Il existe de nombreux scénarios, lorsque vous devez laisser Copy Local 'à True, par exemple

  • Projets de haut niveau,
  • Dépendances de second niveau,
  • DLL appelées par réflexion

Les problèmes possibles décrits dans les questions du SO
" Quand doit-il être défini sur true et quand ne devrait-il pas être défini? ",
" Message d'erreur" Impossible de charger un ou plusieurs des types demandés. Récupérez la propriété LoaderExceptions pour plus d'informations. " »
et   la réponse d ' aaron-stainback  à cette question.

Mon expérience avec la définition de CopyLocal = false n'a PAS réussi. Voir mon article de blog "Ne pas modifier les références de projet" Copier local "à false, sauf si vous comprenez les sous-séquences."

Le temps de résoudre les problèmes surpondère les avantages de la définition de copyLocal = false.

Michael Freidgeim
la source
Le réglage CopyLocal=Falsecausera certainement des problèmes, mais il existe des solutions à ceux-ci. Aussi, vous devriez corriger le formatage de votre blog, il est à peine lisible, et dire là que "j'ai été averti par <random consultant> de <random company> des erreurs possibles lors des déploiements" n'est pas un argument. Vous devez vous développer.
Suzanne Dupéron
@ GeorgesDupéron, Le temps de résoudre les problèmes surpondère les avantages de la définition de copyLocal = false. La référence au consultant n'est pas un argument, mais un crédit, et mon blog explique quels sont les problèmes. Merci pour vos commentaires re. formatage, je vais le réparer.
Michael Freidgeim
1

J'ai tendance à construire dans un répertoire commun (par exemple .. \ bin), donc je peux créer de petites solutions de test.

Kenny
la source
1

Vous pouvez essayer d'utiliser un dossier dans lequel tous les assemblys partagés entre les projets seront copiés, puis créer une variable d'environnement DEVPATH et la définir

<developmentMode developerInstallation="true" />

dans le fichier machine.config sur le poste de travail de chaque développeur. La seule chose que vous devez faire est de copier toute nouvelle version dans votre dossier où pointe la variable DEVPATH.

Divisez également votre solution en quelques solutions plus petites si possible.

Aleksandar
la source
Intéressant ... Comment cela fonctionnerait-il avec les versions debug vs release?
Dave Moore
Je ne suis pas sûr qu'il existe une solution appropriée pour charger des assemblys de débogage / publication via un DEVPATH, il est destiné à être utilisé uniquement pour les assemblys partagés, je ne le recommanderais pas pour créer des versions régulières. Sachez également que la version de l'assembly et le GAC sont remplacés lors de l'utilisation de cette technique.
Aleksandar
1

Ce n'est peut-être pas la meilleure pratique, mais c'est ainsi que je travaille.

J'ai remarqué que Managed C ++ vidait tous ses binaires dans $ (SolutionDir) / 'DebugOrRelease'. J'ai donc vidé tous mes projets C # là aussi. J'ai également désactivé la "Copie locale" de toutes les références aux projets dans la solution. J'ai eu une amélioration notable du temps de construction dans ma petite solution de 10 projets. Cette solution est un mélange de projets C #, C ++ géré, C ++ natif, C # webservice et d'installation.

Peut-être que quelque chose est cassé, mais comme c'est la seule façon de travailler, je ne le remarque pas.

Il serait intéressant de savoir ce que je brise.

jyoung
la source
0

Habituellement, vous n'avez besoin de Copier Local que si vous voulez que votre projet utilise la DLL qui se trouve dans votre Bin par rapport à ce qui se trouve ailleurs (le GAC, d'autres projets, etc.)

J'aurais tendance à être d'accord avec les autres pour dire que vous devriez également essayer, si possible, de briser cette solution.

Vous pouvez également utiliser Configuration Manager pour créer vous-même différentes configurations de construction au sein de cette solution qui ne construira que des ensembles de projets donnés.

Il semblerait étrange que les 100 projets reposent les uns sur les autres, vous devriez donc pouvoir soit le diviser, soit utiliser Configuration Manager pour vous aider.

CubainX
la source
0

Vous pouvez avoir vos références de projets pointant vers les versions de débogage des dll. Que sur votre script msbuild, vous pouvez définir le /p:Configuration=Release, ainsi vous aurez une version finale de votre application et de tous les assemblys satellites.

Bruno Shine
la source
1
Bruno - oui, cela fonctionne avec les références de projets, ce qui est l'une des raisons pour lesquelles nous nous sommes retrouvés avec une solution de 100 projets en premier lieu. Cela ne fonctionne pas sur les références où je navigue vers les versions de débogage pré-construites - je me retrouve avec une application Release construite sur les assemblys Debug, ce qui est un problème
Dave Moore
4
Editez votre fichier projet dans un éditeur de texte et utilisez $ (Configuration) dans votre HintPath, par exemple <HintPath> .. \ output \ $ (Configuration) \ test.dll </HintPath>.
ultravelocity
0

Si la référence n'est pas contenue dans le GAC, nous devons définir Copy Local sur true pour que l'application fonctionne, si nous sommes sûrs que la référence sera préinstallée dans le GAC, elle peut être définie sur false.

SharpGobi
la source
0

Eh bien, je ne sais certainement pas comment les problèmes fonctionnent, mais j'ai été en contact avec une solution de construction qui s'est aidée de telle sorte que tous les fichiers créés ont été placés sur un disque virtuel à l'aide de liens symboliques .

  • c: \ dossier solution \ bin -> ramdisk r: \ dossier solution \ bin \
  • c: \ dossier solution \ obj -> ramdisk r: \ dossier solution \ obj \

  • Vous pouvez également indiquer au studio visuel quel répertoire temporaire il peut utiliser pour la construction.

En fait, ce n'était pas tout ce qu'il faisait. Mais cela a vraiment touché ma compréhension de la performance.
Utilisation à 100% du processeur et un énorme projet en moins de 3 minutes avec toutes les dépendances.


la source