¿Cuál es la mejor práctica para "Copiar local" y con referencias de proyectos?

154

Tengo un gran archivo de solución de C # (~ 100 proyectos), y estoy tratando de mejorar los tiempos de compilación. Creo que "Copiar local" es un desperdicio en muchos casos para nosotros, pero me pregunto sobre las mejores prácticas.

En nuestro .sln, tenemos la aplicación A que depende del conjunto B, que depende del conjunto C. En nuestro caso, hay docenas de "B" y un puñado de "C". Como todos están incluidos en el archivo .sln, estamos utilizando referencias de proyectos. Todos los ensamblados actualmente se integran en $ (SolutionDir) / Debug (o Release).

De forma predeterminada, Visual Studio marca estas referencias de proyecto como "Copiar local", lo que hace que cada "C" se copie en $ (SolutionDir) / Debug una vez por cada "B" que se genera. Esto parece un desperdicio. ¿Qué puede salir mal si solo apago "Copiar local"? ¿Qué hacen otras personas con sistemas grandes?

SEGUIMIENTO:

Muchas respuestas sugieren dividir la compilación en archivos .sln más pequeños ... En el ejemplo anterior, construiría primero las clases básicas "C", seguidas por la mayor parte de los módulos "B", y luego algunas aplicaciones ". UNA". En este modelo, necesito tener referencias no relacionadas con el proyecto a C desde B. El problema con el que me encuentro allí es que "Debug" o "Release" se intercalan en el camino de la pista y termino construyendo mis versiones de lanzamiento de "B" contra las versiones de depuración de "C".

Para aquellos de ustedes que dividen la compilación en múltiples archivos .sln, ¿cómo manejan este problema?

Dave Moore
fuente
9
Puede hacer que su ruta de sugerencias haga referencia al directorio de depuración o liberación editando el archivo del proyecto directamente. Use $ (Configuración) en lugar de depuración o lanzamiento. Por ejemplo, <HintPath> .. \ output \ $ (Configuration) \ test.dll </HintPath> Esto es una molestia cuando tiene muchas referencias (aunque no debería ser difícil para alguien escribir un complemento para manejar esto).
ultravelocity
44
¿'Copiar local' en Visual Studio es lo mismo que <Private>True</Private>en un csproj?
Coronel Panic
Pero dividir a uno .slnen pequeños rompe el cálculo de interdependencia automática de VS de <ProjectReference/>s. Yo mismo me mudé de varios pequeños .slnmás a uno solo .slnsolo porque VS causa menos problemas de esa manera ... Entonces, ¿tal vez el seguimiento está asumiendo una solución no necesariamente mejor para la pregunta original? ;-)
binki
Solo por curiosidad. ¿Por qué complicar las cosas y tener más de 100 proyectos en primer lugar? ¿Es este un mal diseño o qué?
útilBee
@ColonelPanic Sí. Al menos eso es lo que cambia en el disco cuando cambio esa palanca en la GUI.
Zero3

Respuestas:

85

En un proyecto anterior trabajé con una gran solución con referencias de proyectos y también me topé con un problema de rendimiento. La solución fue triple:

  1. Establezca siempre la propiedad Copiar local en falso y aplique esto mediante un paso personalizado de msbuild

  2. Establezca el directorio de salida para cada proyecto en el mismo directorio (preferiblemente en relación con $ (SolutionDir)

  3. Los objetivos de cs predeterminados que se envían con el marco calculan el conjunto de referencias que se copiarán en el directorio de salida del proyecto que se está creando actualmente. Como esto requiere calcular un cierre transitivo bajo la relación 'Referencias', esto puede ser MUY costoso. Mi solución para esto fue redefinir el GetCopyToOutputDirectoryItemsobjetivo en un archivo de objetivos comunes (p. Ej. Common.targets) Que se importa en cada proyecto después de la importación del Microsoft.CSharp.targets. Como resultado, cada archivo de proyecto se verá así:

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

Esto redujo nuestro tiempo de construcción en un momento dado de un par de horas (principalmente debido a restricciones de memoria), a un par de minutos.

El redefinido GetCopyToOutputDirectoryItemsse puede crear copiando las líneas 2,438–2,450 y 2,474–2,524 de C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targetsen Common.targets.

Para completar, la definición de objetivo resultante se convierte en:

<!-- 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>

Con esta solución en su lugar, me pareció factible tener hasta> 120 proyectos en una solución, esto tiene el beneficio principal de que VS puede determinar el orden de construcción de los proyectos en lugar de hacerlo a mano dividiendo su solución .

Bas Bossink
fuente
¿Puedes describir los cambios que hiciste y por qué? Mis ojos están demasiado cansados ​​después de un largo día de codificación para tratar de hacer ingeniería inversa yo mismo :)
Charlie Flowers
¿Qué tal si intentas copiar y pegar esto de nuevo? TAN mal como el 99% de las etiquetas.
ZXX
@Charlie Flowers, @ZXX editó el texto para que sea una descripción que no pudo lograr que el xml tuviera un diseño agradable.
Bas Bossink
1
Desde Microsoft.Common.targets: GetCopyToOutputDirectoryItems Obtenga todos los elementos del proyecto que pueden necesitar transferirse al directorio de salida. Esto incluye artículos de equipaje de proyectos referenciados transitivamente. Parece que este objetivo calcula el cierre transitivo completo de los elementos de contenido para todos los proyectos referenciados; Sin embargo, este no es el caso.
Brans Ds
2
Solo recopila los elementos de contenido de sus hijos inmediatos y no hijos de niños. La razón por la que esto sucede es que la lista ProjectReferenceWithConfiguration que consume _SplitProjectReferencesByFileExistence solo se llena en el proyecto actual y está vacía en los elementos secundarios. La lista vacía hace que _MSBuildProjectReferenceExistent esté vacío y finaliza la recursión. Por lo tanto, parece que no es útil.
Brans Ds
32

Le sugiero que lea los artículos de Patric Smacchia sobre ese tema:

Los proyectos CC.Net VS se basan en la opción de montaje de referencia de copia local establecida en verdadero. [...] No solo esto aumenta significativamente el tiempo de compilación (x3 en el caso de NUnit), sino que también arruina su entorno de trabajo. Por último, pero no menos importante, hacerlo presenta el riesgo de versionar problemas potenciales. Por cierto, NDepend emitirá una advertencia si encuentra 2 ensamblajes en 2 directorios diferentes con el mismo nombre, pero no el mismo contenido o versión.

Lo correcto es definir 2 directorios $ RootDir $ \ bin \ Debug y $ RootDir $ \ bin \ Release, y configurar sus proyectos de VisualStudio para emitir ensamblados en estos directorios. Todas las referencias de proyecto deben hacer referencia a ensamblajes en el directorio de depuración.

También puede leer este artículo para ayudarlo a reducir el número de proyectos y mejorar el tiempo de compilación.

Julien Hoarau
fuente
1
¡Ojalá pudiera recomendar las prácticas de Smacchia con más de un voto a favor! Reducir el número de proyectos es clave, no dividir la solución.
Anthony Mastrean
23

Sugiero tener una copia local = false para casi todos los proyectos, excepto el que está en la parte superior del árbol de dependencias. Y para todas las referencias en el conjunto superior, copie local = true. Veo a muchas personas sugiriendo compartir un directorio de salida; Creo que esta es una idea horrible basada en la experiencia. Si su proyecto de inicio contiene referencias a un archivo DLL que cualquier otro proyecto tiene una referencia a usted, en algún momento experimentará una violación de acceso / uso compartido, incluso si copia local = false en todo y su compilación fallará. Este problema es muy molesto y difícil de localizar. Sugiero completamente que se mantenga alejado de un directorio de salida de fragmentos y, en lugar de tener el proyecto en la parte superior de la cadena de dependencia, escriba los ensamblados necesarios en la carpeta correspondiente. Si no tiene un proyecto en la "parte superior", entonces sugeriría una copia posterior a la compilación para que todo esté en el lugar correcto. Además, trataría de tener en cuenta la facilidad de depuración. Cualquier proyecto exe que todavía deje copy local = true para que la experiencia de depuración F5 funcione.

Aaron Stainback
fuente
2
Tuve esta misma idea y esperaba encontrar a alguien más que pensara lo mismo aquí; Sin embargo, tengo curiosidad por qué esta publicación no tiene más votos a favor. Personas que no están de acuerdo: ¿por qué no están de acuerdo?
Bwerks
No, eso no puede suceder a menos que el mismo proyecto se esté construyendo dos veces, ¿por qué se sobrescribirá \ acceso \ violación de intercambio si se construye una vez y no copia ningún archivo?
Paul
Esta. Si el flujo de trabajo de desarrollo requiere construir un proyecto de la sln, mientras se ejecuta otro proyecto ejecutable de la solución, tener todo en el mismo directorio de salida será un desastre. Mucho mejor separar las carpetas de salida ejecutables en este caso.
Martin Ba
10

Estás en lo correcto. CopyLocal matará absolutamente tus tiempos de construcción. Si tiene un árbol de origen grande, debe deshabilitar CopyLocal. Desafortunadamente, no es tan fácil como debería ser deshabilitarlo limpiamente. He respondido esta pregunta exacta acerca de deshabilitar CopyLocal en Cómo anulo la configuración de CopyLocal (Privado) para referencias en .NET desde MSBUILD . Echale un vistazo. Tanto como mejores prácticas para grandes soluciones en Visual Studio (2008).

Aquí hay más información sobre CopyLocal tal como la veo.

CopyLocal se implementó realmente para admitir la depuración local. Cuando prepare su aplicación para el empaquetado y la implementación, debe compilar sus proyectos en la misma carpeta de salida y asegurarse de tener todas las referencias que necesita allí.

Escribí sobre cómo lidiar con la construcción de grandes árboles de origen en el artículo MSBuild: Mejores prácticas para crear compilaciones confiables, Parte 2 .

Sayed Ibrahim Hashimi
fuente
8

En mi opinión, tener una solución con 100 proyectos es un GRAN error. Probablemente podría dividir su solución en pequeñas unidades lógicas válidas, lo que simplificaría tanto el mantenimiento como las compilaciones.

Bruno Shine
fuente
1
Bruno, consulta mi pregunta de seguimiento anterior: si dividimos en archivos .sln más pequeños, ¿cómo administras el aspecto de Depuración vs. Liberación que luego se integra en la ruta de pistas de mis referencias?
Dave Moore el
1
Estoy de acuerdo con este punto, la solución con la que estoy trabajando tiene ~ 100 proyectos, de los cuales solo unos pocos tienen más de 3 clases, los tiempos de construcción son impactantes y, como resultado, mis predecesores dividieron la solución en 3, lo que rompe por completo 'encontrar todas las referencias 'y refactorización. ¡Todo podría encajar en un puñado de proyectos que se construirían en segundos!
Jon M
Dave, buena pregunta. Donde trabajo, tenemos scripts de compilación que hacen cosas como construir dependencias para una solución dada y colocar los binarios en algún lugar donde la solución en cuestión pueda obtenerlos. Estos scripts están parametrizados para las versiones de depuración y lanzamiento. La desventaja es el tiempo adicional por adelantado para construir dichos scripts, pero pueden reutilizarse en todas las aplicaciones. Esta solución ha funcionado bien según mis estándares.
jyoungdev
7

Me sorprende que nadie haya mencionado el uso de enlaces duros. En lugar de copiar los archivos, crea un enlace rígido al archivo original. Esto ahorra espacio en el disco y acelera enormemente la compilación. Esto se puede habilitar en la línea de comando con las siguientes propiedades:

/ p: CreateHardLinksForAdditionalFilesIfPossible = true; CreateHardLinksForCopyAdditionalFilesIfPossible = true; CreateHardLinksForCopyFilesToOutputDirectoryIfPossible = true; CreateHardLinksForCopyLocalIfPossible = true; CreateHardLinksInterPub

También puede agregar esto a un archivo de importación central para que todos sus proyectos también puedan obtener este beneficio.

Matt Slagle
fuente
5

Si definió la estructura de dependencia a través de referencias de proyecto o dependencias de nivel de solución, es seguro activar "Copiar local". Incluso diría que es una práctica recomendada, ya que eso le permitirá usar MSBuild 3.5 para ejecutar su compilación en paralelo ( via / maxcpucount) sin procesos diferentes que se tropiecen entre sí al intentar copiar ensamblados a los que se hace referencia.

Torbjörn Gyllebring
fuente
4

nuestra "mejor práctica" es evitar soluciones con muchos proyectos. Tenemos un directorio llamado "matriz" con versiones actuales de ensamblajes, y todas las referencias son de este directorio. Si cambia algún proyecto y puede decir "ahora el cambio está completo", puede copiar el ensamblaje en el directorio "matriz". Por lo tanto, todos los proyectos que dependan de este ensamblado tendrán la versión actual (= última).

Si tiene pocos proyectos en solución, el proceso de compilación es mucho más rápido.

Puede automatizar el paso "copiar ensamblado al directorio de matriz" utilizando macros de Visual Studio o con "menú -> herramientas -> herramientas externas ...".

TcKs
fuente
2

Establecer CopyLocal = false reducirá el tiempo de compilación, pero puede causar diferentes problemas durante la implementación.

Hay muchos escenarios, cuando necesita que Copiar Local 'se deje en Verdadero, por ejemplo

  • Proyectos de alto nivel,
  • Dependencias de segundo nivel,
  • DLL llamados por reflexión

Los posibles problemas que se describen en las preguntas SO
"¿ Cuándo se debe establecer copy-local en true y cuándo no? ",
" Mensaje de error 'No se puede cargar uno o más de los tipos solicitados. Recupere la propiedad LoaderExceptions para obtener más información'. "
y   la respuesta de aaron-stainback  a esta pregunta.

Mi experiencia con la configuración de CopyLocal = false NO fue exitosa. Consulte la publicación de mi blog "NO cambiar" las referencias del proyecto Copiar local ”a falso, a menos que comprenda las subsecuencias".

El momento de resolver los problemas sobrepesa los beneficios de configurar copyLocal = false.

Michael Freidgeim
fuente
La configuración CopyLocal=Falseseguramente causará algunos problemas, pero hay soluciones para esos. Además, debe corregir el formato de su blog, apenas es legible, y decir que "me advirtió <asesor aleatorio> de <empresa aleatoria> sobre posibles errores durante las implementaciones" no es un argumento. Necesitas desarrollarte.
Suzanne Dupéron
@ GeorgesDupéron, el momento de resolver los problemas sobrepesa los beneficios de configurar copyLocal = false. La referencia al consultor no es un argumento, sino un crédito, y mi blog explica cuáles son los problemas. Gracias por sus comentarios re. formateo, lo arreglaré.
Michael Freidgeim
1

Tiendo a construir en un directorio común (por ejemplo, \ bin), por lo que puedo crear pequeñas soluciones de prueba.

kenny
fuente
1

Puede intentar usar una carpeta donde se copiarán todos los ensamblajes que se comparten entre proyectos, luego haga una variable de entorno DEVPATH y establezca

<developmentMode developerInstallation="true" />

en el archivo machine.config en la estación de trabajo de cada desarrollador. Lo único que debe hacer es copiar cualquier versión nueva en su carpeta donde apunte la variable DEVPATH.

Divida también su solución en pocas soluciones más pequeñas si es posible.

Aleksandar
fuente
Interesante ... ¿Cómo funcionaría esto con las versiones de depuración y lanzamiento?
Dave Moore
No estoy seguro de si existe alguna solución adecuada para cargar conjuntos de depuración / liberación a través de un DEVPATH, está destinado a ser utilizado solo para conjuntos compartidos, no lo recomendaría para hacer compilaciones regulares. También tenga en cuenta que la versión de ensamblaje y el GAC se anulan al usar esta técnica.
Aleksandar
1

Puede que esta no sea la mejor práctica, pero así es como trabajo.

Noté que Managed C ++ volca todos sus binarios en $ (SolutionDir) / 'DebugOrRelease'. Así que dejé todos mis proyectos de C # allí también. También desactivé la "Copia local" de todas las referencias a proyectos en la solución. Tuve una notable mejora en el tiempo de construcción en mi pequeña solución de 10 proyectos. Esta solución es una mezcla de C #, C ++ administrado, C ++ nativo, servicio web C # y proyectos de instalación.

Tal vez algo está roto, pero como esta es la única forma en que trabajo, no lo noto.

Sería interesante descubrir lo que estoy rompiendo.

jyoung
fuente
0

Por lo general, solo necesita Copiar Local si desea que su proyecto use la DLL que está en su Bin frente a lo que está en otro lugar (el GAC, otros proyectos, etc.)

Tendería a estar de acuerdo con las otras personas en que también debería intentar, si es posible, romper esa solución.

También puede usar el Administrador de configuración para crear diferentes configuraciones de compilación dentro de esa solución que solo creará conjuntos de proyectos dados.

Parecería extraño si los 100 proyectos dependieran el uno del otro, por lo que deberías poder dividirlo o usar Configuration Manager para ayudarte.

CubanX
fuente
0

Puede hacer que sus referencias de proyectos apunten a las versiones de depuración de los dlls. Que en su secuencia de comandos msbuild, puede configurar el /p:Configuration=Release, por lo que tendrá una versión de lanzamiento de su aplicación y todos los conjuntos de satélites.

Bruno Shine
fuente
1
Bruno: sí, esto funciona con Referencias de proyectos, que es una de las razones por las que terminamos con una solución de 100 proyectos en primer lugar. No funciona en referencias en el que vaya a la depuración de comunicados de pre-construidos - termino con una aplicación de lanzamiento construido en contra de las asambleas de depuración, que es un problema
de Dave Moore
44
Edite su archivo de proyecto en un editor de texto y use $ (Configuración) en su HintPath, por ejemplo, <HintPath> .. \ output \ $ (Configuration) \ test.dll </HintPath>.
ultravelocity
0

Si la referencia no está contenida en el GAC, debemos establecer la Copia local en verdadero para que la aplicación funcione, si estamos seguros de que la referencia estará preinstalada en el GAC, entonces se puede establecer en falso.

SharpGobi
fuente
0

Bueno, ciertamente no sé cómo funcionan los problemas, pero tuve contacto con una solución de compilación que se ayudó de tal manera que todos los archivos creados se pusieron en un disco RAM con la ayuda de enlaces simbólicos .

  • c: \ solution folder \ bin -> ramdisk r: \ solution folder \ bin \
  • c: \ carpeta de soluciones \ obj -> ramdisk r: \ carpeta de soluciones \ obj \

  • También puede indicarle al estudio visual qué directorio temporal puede usar para la compilación.

En realidad, eso no fue todo lo que hizo. Pero realmente me impactó mi comprensión del rendimiento.
100% de uso del procesador y un gran proyecto en menos de 3 minutos con todas las dependencias.


fuente