Qual é a melhor prática para “Copiar local” e com referências de projeto?

154

Eu tenho um arquivo de solução c # grande (~ 100 projetos) e estou tentando melhorar os tempos de compilação. Eu acho que "Copiar local" é um desperdício em muitos casos para nós, mas estou pensando nas melhores práticas.

Em nosso .sln, temos o aplicativo A, dependendo do conjunto B, que depende do conjunto C. No nosso caso, existem dezenas de "B" e um punhado de "C". Como todos estão incluídos no .sln, estamos usando referências de projeto. Todos os assemblies atualmente compilam em $ (SolutionDir) / Debug (ou Release).

Por padrão, o Visual Studio marca essas referências de projeto como "Copiar Local", o que resulta em cada "C" sendo copiado em $ (SolutionDir) / Debug uma vez para cada "B" criado. Isso parece um desperdício. O que pode dar errado se eu desligar a opção "Copiar local"? O que outras pessoas com sistemas grandes fazem?

ACOMPANHAMENTO:

Muitas respostas sugerem dividir a compilação em arquivos .sln menores ... No exemplo acima, eu criaria as classes de fundação "C" primeiro, seguidas pela maior parte dos módulos "B" e depois alguns aplicativos " UMA". Nesse modelo, preciso ter referências que não sejam do projeto a C de B. O problema que encontro é que "Debug" ou "Release" são inseridos no caminho da dica e acabo criando minhas compilações de Release "B" contra compilações de depuração de "C".

Para aqueles de vocês que dividiram a compilação em vários arquivos .sln, como gerencia esse problema?

Dave Moore
fonte
9
Você pode fazer com que seu caminho de dica faça referência ao diretório Debug ou Release editando o arquivo do projeto diretamente. Use $ (Configuration) no lugar de Debug ou Release. Por exemplo, <HintPath> .. \ output \ $ (Configuration) \ test.dll </HintPath> Isso é um problema quando você tem muitas referências (embora não deva ser difícil para alguém escrever um suplemento no gerenciar isso).
#
4
'Copiar local' no Visual Studio é o mesmo que <Private>True</Private>em um csproj?
Coronel Panic
Mas dividir a .slnem menor quebra o cálculo da interdependência automagica de VS de <ProjectReference/>s. Mudei de vários .slns menores para um grande .slnsó porque o VS causa menos problemas dessa maneira ... Então, talvez o acompanhamento esteja assumindo uma solução não necessariamente a melhor para a pergunta original? ;-)
binki
Apenas por curiosidade. Por que complicar as coisas e ter mais de 100 projetos em primeiro lugar? Esse design é ruim ou o quê?
usar o seguinte código
@ColonelPanic Sim. Pelo menos, é isso que muda no disco quando eu mudo essa alternância na GUI.
Zero3 28/08

Respostas:

85

Em um projeto anterior, trabalhei com uma grande solução com referências de projeto e me deparei com um problema de desempenho. A solução foi três vezes:

  1. Sempre defina a propriedade Copy Local como false e imponha isso por meio de uma etapa personalizada do msbuild

  2. Defina o diretório de saída de cada projeto para o mesmo diretório (de preferência em relação a $ (SolutionDir)

  3. Os destinos cs padrão que são enviados com a estrutura calculam o conjunto de referências a serem copiadas para o diretório de saída do projeto que está sendo construído atualmente. Como isso requer o cálculo de um fechamento transitivo sob a relação 'Referências', isso pode se tornar MUITO dispendioso. Minha solução para isso foi redefinir o GetCopyToOutputDirectoryItemsdestino em um arquivo de destinos comum (por exemplo, Common.targets) importado em todos os projetos após a importação do Microsoft.CSharp.targets. Resultando em cada arquivo de projeto com a seguinte aparência:

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

Isso reduziu nosso tempo de compilação em um determinado momento de algumas horas (principalmente devido a restrições de memória), para alguns minutos.

A redefinição GetCopyToOutputDirectoryItemspode ser criada copiando as linhas 2.438-2.450 e 2.474-2.524 de C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targetspara Common.targets.

Para completar, a definição de destino resultante se torna:

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

Com essa solução alternativa, achei viável ter até 120 projetos em uma solução, isso tem o principal benefício de que a ordem de criação dos projetos ainda pode ser determinada pelo VS em vez de fazê-lo manualmente dividindo sua solução .

Bas Bossink
fonte
Você pode descrever as alterações feitas e por quê? Meus olhos estão muito cansados depois de um longo dia de codificação para tentar fazer engenharia reversa me :)
Charlie Flowers
Que tal tentar copiar e colar isso de novo - SO desarrumado como 99% das tags.
ZXX 27/08/10
@Charlie Flowers, o @ZXX editou o texto para ser uma descrição. Não foi possível obter o layout do XML muito bem.
Bas Bossink
1
No Microsoft.Common.targets: GetCopyToOutputDirectoryItems Obtenha todos os itens de projeto que talvez precisem ser transferidos para o diretório de saída. Isso inclui itens de bagagem de projetos referenciados transitivamente. Parece que esse destino calcula o fechamento transitivo total dos itens de conteúdo para todos os projetos referenciados; no entanto, esse não é o caso.
quer
2
Ele coleta apenas os itens de conteúdo de seus filhos imediatos e não filhos de filhos. O motivo é que a lista ProjectReferenceWithConfiguration que é consumida por _SplitProjectReferencesByFileExistence é preenchida apenas no projeto atual e está vazia nos filhos. A lista vazia faz com que _MSBuildProjectReferenceExistent fique vazio e encerre a recursão. Portanto, parece não ser útil.
amigos estão dizendo
32

Vou sugerir que você leia os artigos de Patric Smacchia sobre esse assunto:

Os projetos do CC.Net VS contam com a opção de copiar local de montagem de referência definida como true. [...] Não apenas isso aumenta significativamente o tempo de compilação (x3 no caso do NUnit), mas também atrapalha o seu ambiente de trabalho. Por último, mas não menos importante, fazer isso apresenta o risco de versionar problemas em potencial. Aliás, o NDepend emitirá um aviso se localizar 2 assemblies em 2 diretórios diferentes com o mesmo nome, mas não com o mesmo conteúdo ou versão.

A coisa certa a fazer é definir 2 diretórios $ RootDir $ \ bin \ Debug e $ RootDir $ \ bin \ Release e configurar seus projetos do VisualStudio para emitir assemblies nesses diretórios. Todas as referências de projeto devem fazer referência a assemblies no diretório Debug.

Você também pode ler este artigo para ajudar a reduzir o número de projetos e melhorar o tempo de compilação.

Julien Hoarau
fonte
1
Eu gostaria de poder recomendar as práticas de Smacchia com mais de um voto positivo! Reduzir o número de projetos é fundamental, não dividir a solução.
Anthony Mastrean
23

Sugiro ter cópia local = false para quase todos os projetos, exceto o que está no topo da árvore de dependência. E para todas as referências no conjunto superior, copie local = true. Vejo muitas pessoas sugerindo o compartilhamento de um diretório de saída; Eu acho que essa é uma ideia horrível baseada na experiência. Se o seu projeto de inicialização contiver referências a uma DLL que qualquer outro projeto contenha uma referência a você, em algum momento, ocorrerá uma violação de acesso / compartilhamento, mesmo que a cópia local = false em tudo e sua compilação falhe. Esse problema é muito chato e difícil de rastrear. Sugiro completamente ficar longe de um diretório de saída shard e, em vez de ter o projeto no topo da cadeia de dependência, grave os assemblies necessários na pasta correspondente. Se você não tem um projeto na parte superior, então eu sugiro uma cópia pós-compilação para colocar tudo no lugar certo. Além disso, eu tentaria ter em mente a facilidade da depuração. Quaisquer projetos exe ainda deixo copy local = true para que a experiência de depuração F5 funcione.

Aaron Stainback
fonte
2
Eu tive essa mesma ideia e esperava encontrar outra pessoa que pensasse da mesma maneira aqui; no entanto, estou curioso para saber por que esse post não tem mais votos positivos. Pessoas que discordam: por que você discorda?
bwerks
Isso não pode acontecer, a menos que o mesmo projeto esteja sendo construído duas vezes, por que ele seria substituído por violação de \ access \ sharing se for construído uma vez e não copie nenhum arquivo?
1913 paulm
Este. Se o fluxo de trabalho de desenvolvimento exigir a construção de um projeto do sln, enquanto outro projeto executável da solução estiver em execução, ter tudo no mesmo diretório de saída será uma bagunça. Muito melhor para separar pastas de saída executáveis ​​nesse caso.
Martin Ba
10

Você está certo. CopyLocal absolutamente matará seus tempos de construção. Se você tiver uma grande árvore de origem, desative o CopyLocal. Infelizmente, não é tão fácil quanto deve ser desabilitá-lo corretamente. Respondi a essa pergunta exata sobre como desativar o CopyLocal em Como substituir a configuração de CopyLocal (particular) para referências no .NET do MSBUILD . Confira. Bem como práticas recomendadas para grandes soluções no Visual Studio (2008).

Aqui estão mais algumas informações sobre o CopyLocal como eu o vejo.

O CopyLocal foi implementado realmente para oferecer suporte à depuração local. Ao preparar seu aplicativo para empacotamento e implantação, você deve criar seus projetos na mesma pasta de saída e ter todas as referências necessárias lá.

Escrevi sobre como lidar com a criação de grandes árvores de origem no artigo MSBuild: Práticas recomendadas para criar construções confiáveis, Parte 2 .

Sayed Ibrahim Hashimi
fonte
8

Na minha opinião, ter uma solução com 100 projetos é um GRANDE erro. Você provavelmente poderia dividir sua solução em pequenas unidades lógicas válidas, simplificando a manutenção e as compilações.

Bruno Shine
fonte
1
Bruno, veja minha pergunta de acompanhamento acima - se dividirmos em arquivos .sln menores, como você gerencia o aspecto Debug vs. Release, que é inserido no caminho das dicas de minhas referências?
Dave Moore
1
Eu concordo com este ponto, a solução com a qual estou trabalhando tem ~ 100 projetos, dos quais apenas alguns têm mais de 3 classes, os tempos de construção são chocantes e, como resultado, meus antecessores dividiram a solução em 3 que quebram completamente a descoberta. todas as referências 'e refatoração. A coisa toda poderia caber em um punhado de projetos que seriam construídos em segundos!
Jon M
Dave, boa pergunta. Onde trabalho, criamos scripts que fazem coisas como criar dependências para uma determinada solução e colocamos os binários em algum lugar onde a solução em questão possa obtê-los. Esses scripts são parametrizados para compilações de depuração e versão. A desvantagem é o tempo extra antecipado para criar os scripts, mas eles podem ser reutilizados nos aplicativos. Esta solução funcionou bem para os meus padrões.
precisa saber é o seguinte
7

Estou surpreso que ninguém tenha mencionado o uso de hardlinks. Em vez de copiar os arquivos, ele cria um link físico para o arquivo original. Isso economiza espaço em disco e acelera bastante a compilação. Isso pode ser ativado na linha de comando com as seguintes propriedades:

/ p: CreateHardLinksForAdditionalFilesIfPossible = true; CreateHardLinksForCopyAdditionalFilesIfPossible = true; CreateHardLinksForCopyFilesToOutputDirectoryIfPossible = true; CreateHardLinksForCopyLossIfPossible = true; CreateHardLinksForPublishFilesIf

Você também pode adicionar isso a um arquivo de importação central, para que todos os seus projetos também possam obter esse benefício.

Matt Slagle
fonte
5

Se você definiu a estrutura de dependência por meio de referências de projeto ou dependências em nível de solução, é seguro ativar "Copy Local", diria até que é uma prática recomendada, pois isso permitirá que você use o MSBuild 3.5 para executar sua compilação paralelamente ( via / maxcpucount) sem processos diferentes que tropeçam entre si ao tentar copiar montagens referenciadas.

Torbjörn Gyllebring
fonte
4

nossa "melhor prática" é evitar soluções em muitos projetos. Temos um diretório chamado "matrix" com versões atuais de assemblies e todas as referências são desse diretório. Se você alterar algum projeto e puder dizer "agora a alteração está concluída", poderá copiar a montagem no diretório "matriz". Portanto, todos os projetos que dependem dessa montagem terão a versão atual (= mais recente).

Se você tem poucos projetos em solução, o processo de criação é muito mais rápido.

Você pode automatizar a etapa "copiar montagem no diretório da matriz" usando macros do visual studio ou com "menu -> ferramentas -> ferramentas externas ...".

TcKs
fonte
2

Definir CopyLocal = false reduzirá o tempo de construção, mas poderá causar problemas diferentes durante a implantação.

Existem muitos cenários, quando você precisa que a opção Copiar local seja deixada como True, por exemplo,

  • Projetos de nível superior,
  • Dependências de segundo nível,
  • DLLs chamadas por reflexão

Os possíveis problemas descritos nas perguntas do SO
" Quando local de cópia deve ser definido como verdadeiro e quando não deve? ",
" Mensagem de erro 'Não é possível carregar um ou mais dos tipos solicitados. Recupere a propriedade LoaderExceptions para obter mais informações.' "
e   a resposta de aaron-stainback  para esta pergunta.

Minha experiência com a configuração de CopyLocal = false NÃO foi bem-sucedida. Veja no meu blog "NÃO alterar" copiar local "as referências do projeto para false, a menos que compreenda subsequências."

O tempo para resolver os problemas excedem os benefícios da configuração de copyLocal = false.

Michael Freidgeim
fonte
A configuração CopyLocal=Falsecertamente causará alguns problemas, mas existem soluções para eles. Além disso, você deve corrigir a formatação do seu blog, dificilmente legível, e dizer lá que "fui avisado pelo <consultor aleatório> da <empresa aleatória> sobre possíveis erros durante as implantações" não é um argumento. Você precisa se desenvolver.
Suzanne Dupéron 20/03/2013
@ GeorgesDupéron, O tempo para resolver os problemas excedem os benefícios de definir copyLocal = false. A referência ao consultor não é um argumento, mas um crédito, e meu blog explica quais são os problemas. Obrigado pelo seu feedback re. formatação, eu vou corrigi-lo.
22713 Michael Freidgeim
1

Costumo construir em um diretório comum (por exemplo, .. \ bin), para poder criar pequenas soluções de teste.

kenny
fonte
1

Você pode tentar usar uma pasta na qual todos os assemblies compartilhados entre projetos serão copiados, criar uma variável de ambiente DEVPATH e definir

<developmentMode developerInstallation="true" />

no arquivo machine.config na estação de trabalho de cada desenvolvedor. A única coisa que você precisa fazer é copiar qualquer nova versão na pasta onde a variável DEVPATH aponte.

Divida também sua solução em poucas soluções menores, se possível.

Aleksandar
fonte
Interessante ... Como isso funcionaria com compilações de depuração vs. versão?
Dave Moore
Não tenho certeza se existe alguma solução adequada para carregar assemblies de depuração / liberação por meio de um DEVPATH, ela deve ser usada apenas para assemblies compartilhados, não a recomendo para criar compilações regulares. Lembre-se também de que a versão do assembly e o GAC são substituídos ao usar esta técnica.
11288 Aleksandar
1

Pode não ser a melhor prática, mas é assim que eu trabalho.

Notei que o C ++ gerenciado despeja todos os seus binários em $ (SolutionDir) / 'DebugOrRelease'. Então, eu joguei todos os meus projetos de C # lá também. Também desativei o "Copiar local" de todas as referências a projetos na solução. Eu tive uma melhora notável no tempo de construção em minha pequena solução de 10 projetos. Essa solução é uma mistura de projetos em C #, C ++ gerenciado, C ++ nativo, C # webservice e instalador.

Talvez algo esteja quebrado, mas como essa é a única maneira de trabalhar, não percebo.

Seria interessante descobrir o que estou quebrando.

jyoung
fonte
0

Normalmente, você só precisa copiar o local se quiser que seu projeto use a DLL que está na sua lixeira versus o que está em outro lugar (o GAC, outros projetos etc.)

Eu tenderia a concordar com as outras pessoas que você também deveria tentar, se possível, interromper essa solução.

Você também pode usar o Gerenciador de Configurações para criar diferentes configurações de compilação nessa solução que criará apenas determinados conjuntos de projetos.

Seria estranho se todos os 100 projetos dependessem um do outro, portanto, você poderá dividi-lo ou usar o Gerenciador de Configurações para ajudar a si mesmo.

CubanX
fonte
0

Você pode ter suas referências de projetos apontando para as versões de depuração das DLLs. Do que no seu script msbuild, você pode definir o /p:Configuration=Release, assim você terá uma versão de lançamento do seu aplicativo e de todos os assemblies de satélite.

Bruno Shine
fonte
1
Bruno - sim, isso funciona com as referências do projeto, que é uma das razões pelas quais acabamos com uma solução de 100 projetos em primeiro lugar. Ele não funciona em referências onde eu navegar para a pré-construídos de depuração lançamentos - I acabam com um aplicativo lançamento construída contra os conjuntos de depuração, que é um problema
Dave Moore
4
Edite seu arquivo de projeto em um editor de texto e use $ (Configuration) no HintPath, por exemplo, <HintPath> .. \ output \ $ (Configuration) \ test.dll </HintPath>.
#
0

Se a referência não estiver contida no GAC, devemos definir o Local da cópia como true para que o aplicativo funcione. Se tivermos certeza de que a referência será pré-instalada no GAC, ela poderá ser definida como false.

SharpGobi
fonte
0

Bem, eu certamente não sei como os problemas funcionam, mas eu tive contato com uma solução de compilação que ajudou a si mesma, de modo que todos os arquivos criados foram colocados em um ramdisk com a ajuda de links simbólicos .

  • c: \ pasta de solução \ bin -> ramdisk r: \ pasta de solução \ bin \
  • c: \ pasta de solução \ obj -> ramdisk r: \ pasta de solução \ obj \

  • Você também pode informar adicionalmente ao visual studio qual diretório temporário ele pode usar para a compilação.

Na verdade, não foi tudo o que fez. Mas realmente atingiu minha compreensão do desempenho.
100% de uso do processador e um grande projeto em menos de 3 minutos com todas as dependências.


fonte