Qual è la migliore pratica per "Copia locale" e con riferimenti al progetto?


154

Ho un grande file di soluzione c # (~ 100 progetti) e sto cercando di migliorare i tempi di compilazione. Penso che "Copy Local" sia uno spreco in molti casi per noi, ma mi chiedo quali siano le migliori pratiche.

Nel nostro .sln, abbiamo l'applicazione A che dipende dal gruppo B che dipende dal gruppo C. Nel nostro caso, ci sono dozzine di "B" e una manciata di "C". Dato che questi sono tutti inclusi in .sln, stiamo usando riferimenti di progetto. Tutti gli assembly attualmente compilano in $ (SolutionDir) / Debug (o Release).

Per impostazione predefinita, Visual Studio contrassegna questi riferimenti di progetto come "Copia locale", il che comporta che ogni "C" viene copiata in $ (SolutionDir) / Debug una volta per ogni "B" che viene generata. Sembra uno spreco. Cosa può andare storto se disattivo "Copia locale"? Cosa fanno le altre persone con sistemi di grandi dimensioni?

AZIONE SUPPLEMENTARE:

Molte risposte suggeriscono di suddividere la build in file .sln più piccoli ... Nell'esempio sopra, prima creerei le classi di base "C", seguite dalla maggior parte dei moduli "B", e quindi alcune applicazioni " UN". In questo modello, ho bisogno di avere riferimenti non di progetto a C da B. Il problema in cui mi imbatto è che "Debug" o "Release" vengono inseriti nel percorso del suggerimento e finisco per costruire le build di "B" di Release contro build di debug di "C".

Per quelli di voi che hanno diviso il build up in più file .sln, come gestite questo problema?


9
È possibile fare in modo che il percorso del suggerimento faccia riferimento alla directory Debug o Release modificando direttamente il file di progetto. Utilizzare $ (Configurazione) al posto di Debug o Rilascio. Ad esempio, <HintPath> .. \ output \ $ (Configuration) \ test.dll </HintPath> Questo è un problema quando hai molti riferimenti (anche se non dovrebbe essere difficile per qualcuno scrivere un componente aggiuntivo in gestirlo).
ultravelocità

4
"Copia locale" in Visual Studio è lo stesso <Private>True</Private>di un csproj?
Colonnello Panic,

La suddivisione di una .slnin una più piccola interrompe il calcolo automagico dell'interdipendenza di VS di <ProjectReference/>s. Sono passato da più piccoli .slnpiù a un singolo grande .slnsolo perché VS causa meno problemi in quel modo ... Quindi, forse il follow-up sta assumendo una soluzione non necessariamente migliore alla domanda originale? ;-)
binki,

Solo per curiosità. Perché complicare le cose e avere oltre 100 progetti al primo posto? Questo cattivo design o cosa?
utileBee

@ColonelPanic Sì. Almeno questa è la cosa che cambia sul disco quando cambio quell'interruttore nella GUI.
Zero3,

Risposte:


85

In un precedente progetto ho lavorato con una grande soluzione con riferimenti di progetto e ho anche riscontrato un problema di prestazioni. La soluzione era tre volte:

  1. Impostare sempre la proprietà Copia locale su false e applicarla tramite un passaggio msbuild personalizzato

  2. Impostare la directory di output per ciascun progetto sulla stessa directory (preferibilmente rispetto a $ (SolutionDir)

  3. Le destinazioni cs predefinite fornite con il framework calcolano la serie di riferimenti da copiare nella directory di output del progetto attualmente in costruzione. Poiché ciò richiede il calcolo di una chiusura transitiva nella relazione "Riferimenti", ciò può diventare MOLTO costoso. La mia soluzione è stata quella di ridefinire il GetCopyToOutputDirectoryItemstarget in un file target comune (ad es. Common.targets) Che è stato importato in ogni progetto dopo l'importazione di Microsoft.CSharp.targets. Il risultato in ogni file di progetto è simile al seguente:

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

Ciò ha ridotto il nostro tempo di costruzione in un determinato momento da un paio d'ore (principalmente a causa di vincoli di memoria), a un paio di minuti.

La ridefinizione GetCopyToOutputDirectoryItemspuò essere creata copiando le linee 2.438–2.450 e 2.474–2.524 da C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targetsin Common.targets.

Per completezza la definizione di destinazione risultante diventa quindi:

<!-- 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 questa soluzione alternativa ho trovato fattibile avere fino a> 120 progetti in una soluzione, questo ha il vantaggio principale che l'ordine di costruzione dei progetti può ancora essere determinato da VS invece di farlo manualmente dividendo la soluzione .


Puoi descrivere le modifiche che hai apportato e perché? I miei occhi sono troppo stanchi dopo una lunga giornata di programmazione per provare a decodificarlo da solo :)
Charlie Flowers

Che ne dici di provare a copiarlo e incollarlo di nuovo - COSÌ incasinato come il 99% dei tag.
ZXX,

@Charlie Flowers, @ZXX ha modificato il testo in modo da renderlo una descrizione non è stato in grado di ottenere un layout XML perfetto.
Bas Bossink,

1
Da Microsoft.Common.targets: GetCopyToOutputDirectoryItems Ottieni tutti gli elementi del progetto che potrebbero dover essere trasferiti nella directory di output. Ciò include gli articoli relativi al bagaglio di progetti referenziati in modo transitorio. Sembrerebbe che questo obiettivo calcoli la chiusura transitiva completa degli elementi di contenuto per tutti i progetti referenziati; tuttavia non è così.
Brans Ds

2
Raccoglie solo gli elementi di contenuto dai suoi figli immediati e non dai figli dei bambini. Il motivo per cui ciò accade è che l'elenco ProjectReferenceWithConfiguration utilizzato da _SplitProjectReferencesByFileExistence viene popolato solo nel progetto corrente ed è vuoto nei bambini. L'elenco vuoto fa sì che _MSBuildProjectReferenceExistent sia vuoto e termina la ricorsione. Quindi non sembra utile.
Brans Ds

32

Ti suggerirò di leggere gli articoli di Patric Smacchia sull'argomento:

I progetti CC.Net VS si basano sull'opzione di copia dell'assieme di riferimento locale impostata su true. [...] Non solo questo aumenta significativamente il tempo di compilazione (x3 nel caso di NUnit), ma rovina anche l'ambiente di lavoro. Ultimo ma non meno importante, farlo introduce il rischio di potenziali problemi di versioning. A proposito, NDepend emetterà un avviso se trova 2 assembly in 2 directory diverse con lo stesso nome, ma non con lo stesso contenuto o versione.

La cosa giusta da fare è definire 2 directory $ RootDir $ \ bin \ Debug e $ RootDir $ \ bin \ Release e configurare i progetti VisualStudio per emettere assembly in queste directory. Tutti i riferimenti al progetto devono fare riferimento agli assembly nella directory Debug.

Puoi anche leggere questo articolo per aiutarti a ridurre il numero dei tuoi progetti e migliorare i tempi di compilazione.


1
Vorrei poter raccomandare le pratiche di Smacchia con più di un voto! Ridurre il numero di progetti è fondamentale, non dividere la soluzione.
Anthony Mastrean,

23

Suggerisco di avere copia local = false per quasi tutti i progetti tranne quello che si trova nella parte superiore dell'albero delle dipendenze. E per tutti i riferimenti in quello in alto, copia local = true. Vedo molte persone suggerire di condividere una directory di output; Penso che questa sia un'idea orribile basata sull'esperienza. Se il tuo progetto di avvio contiene riferimenti a una DLL che qualsiasi altro progetto contiene un riferimento a te, ad un certo punto subirai una violazione di accesso / condivisione anche se copia local = false su tutto e la tua build fallirà. Questo problema è molto fastidioso e difficile da rintracciare. Consiglio vivamente di stare lontano da una directory di output del frammento e invece di avere il progetto in cima alla catena delle dipendenze scrivere gli assembly necessari nella cartella corrispondente. Se non hai un progetto nella parte superiore, quindi suggerirei una copia post-build per ottenere tutto nel posto giusto. Inoltre, vorrei provare a tenere presente la facilità di debug. Qualsiasi progetto exe lascio ancora copia local = true, quindi l'esperienza di debug F5 funzionerà.


2
Avevo la stessa idea e speravo di trovare qualcun altro che la pensasse allo stesso modo qui; tuttavia, sono curioso di sapere perché questo post non ha più voti. Le persone che non sono d'accordo: perché non sei d'accordo?
Bwerks,

No, ciò non può accadere a meno che lo stesso progetto non venga realizzato due volte, perché verrebbe sovrascritto \ access \ violando la condivisione se fosse stato creato una volta e non copiasse alcun file?
paulm,

Questo. Se il flusso di lavoro di sviluppo richiede la costruzione di un progetto dello sln, mentre è in esecuzione un altro progetto della soluzione eseguibile dal progetto, avere tutto nella stessa directory di output sarà un disastro. In questo caso è molto meglio separare le cartelle di output eseguibili.
Martin Ba,

10

Hai ragione. CopyLocal ucciderà assolutamente i tuoi tempi di costruzione. Se si dispone di un albero di origine di grandi dimensioni, è necessario disabilitare CopyLocal. Sfortunatamente non è facile come dovrebbe essere disabilitarlo in modo pulito. Ho risposto a questa domanda esatta sulla disabilitazione di CopyLocal in Come posso sovrascrivere l'impostazione CopyLocal (privato) per i riferimenti in .NET da MSBUILD . Controlla. Oltre alle best practice per soluzioni di grandi dimensioni in Visual Studio (2008).

Ecco alcune altre informazioni su CopyLocal come la vedo io.

CopyLocal è stato implementato per supportare il debug locale. Quando prepari la tua applicazione per il packaging e la distribuzione, dovresti creare i tuoi progetti nella stessa cartella di output e assicurarti di avere tutti i riferimenti necessari lì.

Ho scritto su come gestire la costruzione di alberi di grandi dimensioni nell'articolo MSBuild: best practice per la creazione di build affidabili, parte 2 .


8

Secondo me, avere una soluzione con 100 progetti è un GRANDE errore. Probabilmente potresti dividere la tua soluzione in piccole unità logiche valide, semplificando così sia la manutenzione che le build.


1
Bruno, per favore vedi la mia domanda di follow-up sopra - se dividiamo in file .sln più piccoli, come gestisci l'aspetto Debug vs. Release che viene poi inserito nel percorso di suggerimento dei miei riferimenti?
Dave Moore,

1
Sono d'accordo con questo punto, la soluzione con cui sto lavorando ha ~ 100 progetti, solo una manciata di cui ha più di 3 classi, i tempi di costruzione sono scioccanti e, di conseguenza, i miei predecessori hanno diviso la soluzione in 3 che interrompe completamente tutti i riferimenti 'e refactoring. Il tutto potrebbe rientrare in una manciata di progetti che si costruiranno in pochi secondi!
Jon M

Dave, bella domanda. Dove lavoro, abbiamo script di compilazione che fanno cose come costruire dipendenze per una data soluzione e mettere i binari da qualche parte dove la soluzione in questione può ottenerli. Questi script sono parametrizzati sia per il debug che per le versioni di rilascio. Il rovescio della medaglia è il tempo extra per costruire detti script, ma possono essere riutilizzati tra le app. Questa soluzione ha funzionato bene secondo i miei standard.
jyoungdev,

7

Sono sorpreso che nessuno abbia menzionato l'uso di hardlink. Invece di copiare i file, crea un collegamento reale al file originale. Ciò consente di risparmiare spazio su disco e di accelerare notevolmente la generazione. Questo può essere abilitato sulla riga di comando con le seguenti proprietà:

/ P: CreateHardLinksForAdditionalFilesIfPossible = true; CreateHardLinksForCopyAdditionalFilesIfPossible = true; CreateHardLinksForCopyFilesToOutputDirectoryIfPossible = true; CreateHardLinksForCopyLocalIfPossible = true; CreateHardLinksForPublishFilesIfPossible = true

Puoi anche aggiungerlo a un file di importazione centrale in modo che anche tutti i tuoi progetti possano ottenere questo vantaggio.


5

Se hai la struttura delle dipendenze definita tramite riferimenti di progetto o dipendenze a livello di soluzione, è sicuro passare a "Copia locale", direi anche che è una buona pratica, quindi poiché ti consentirà di utilizzare MSBuild 3.5 per eseguire la build in parallelo ( via / maxcpucount) senza che processi diversi si inciampino l'uno sull'altro quando si tenta di copiare gli assembly referenziati.


4

la nostra "migliore pratica" è quella di evitare soluzioni con molti progetti. Abbiamo una directory denominata "matrice" con le versioni correnti degli assiemi e tutti i riferimenti provengono da questa directory. Se si modifica un progetto e si può dire "ora la modifica è completa", è possibile copiare l'assembly nella directory "matrice". Quindi tutti i progetti che dipendono da questo assembly avranno l'attuale (= ultima) versione.

Se hai pochi progetti in soluzione, il processo di compilazione è molto più veloce.

Puoi automatizzare il passaggio "copia assembly nella directory matrix" usando le macro di Visual Studio o con "menu -> strumenti -> strumenti esterni ...".



2

Set CopyLocal = false ridurrà i tempi di compilazione, ma può causare diversi problemi durante la distribuzione.

Esistono molti scenari in cui è necessario lasciare Copia locale 'su True, ad es

  • Progetti di alto livello,
  • Dipendenze di secondo livello,
  • DLL richiamate da reflection

I possibili problemi descritti nelle domande SO
" Quando dovrebbe essere impostato il valore di copia-locale su true e quando non dovrebbe? ",
" Messaggio di errore" Impossibile caricare uno o più dei tipi richiesti. Recupera la proprietà LoaderExceptions per ulteriori informazioni. " "
e   la risposta di aaron-stainback  per questa domanda.

La mia esperienza con l'impostazione di CopyLocal = false NON è andata a buon fine. Vedi il mio post sul blog "NON cambiare" Copia i riferimenti del progetto locale a false, a meno che non capisca le sottosequenze ".

Il tempo per risolvere i problemi sovrappesa i vantaggi dell'impostazione di copyLocal = false.


L'impostazione CopyLocal=Falsecauserà sicuramente alcuni problemi, ma ci sono soluzioni a questi. Inoltre, dovresti correggere la formattazione del tuo blog, è a malapena leggibile, e dire lì che "Sono stato avvisato da <consulente casuale> da <società casuale> su possibili errori durante le distribuzioni" non è un argomento. Devi sviluppare.
Suzanne Dupéron,

@ GeorgesDupéron, Il tempo per risolvere i problemi sovrappeso ai vantaggi dell'impostazione di copyLocal = false. Il riferimento al consulente non è un argomento, ma credito, e il mio blog spiega quali sono i problemi. Grazie per il tuo feedback. formattazione, lo riparerò.
Michael Freidgeim,

1

Tendo a creare una directory comune (ad es. \ Bin), quindi posso creare piccole soluzioni di test.


1

È possibile provare a utilizzare una cartella in cui verranno copiati tutti gli assembly condivisi tra progetti, quindi creare una variabile d'ambiente DEVPATH e impostare

<developmentMode developerInstallation="true" />

file machine.config sulla workstation di ogni sviluppatore. L'unica cosa che devi fare è copiare qualsiasi nuova versione nella tua cartella in cui punta la variabile DEVPATH.

Se possibile, dividi anche la soluzione in alcune soluzioni più piccole.


Interessante ... Come funzionerebbe con le build di debug vs. release?
Dave Moore,

Non sono sicuro che esista una soluzione adatta per il caricamento di assembly di debug / release attraverso un DEVPATH, è inteso per essere utilizzato solo per assembly condivisi, non lo consiglierei per fare build regolari. Inoltre, tenere presente che la versione dell'assembly e GAC vengono ignorati quando si utilizza questa tecnica.
Aleksandar,

1

Questa potrebbe non essere la migliore pratica, ma è così che lavoro.

Ho notato che Managed C ++ scarica tutti i suoi binari in $ (SolutionDir) / 'DebugOrRelease'. Quindi ho scaricato tutti i miei progetti C # anche lì. Ho anche disattivato il "Copia locale" di tutti i riferimenti ai progetti nella soluzione. Ho notato un notevole miglioramento dei tempi di costruzione nella mia piccola soluzione per 10 progetti. Questa soluzione è una combinazione di progetti C #, C ++ gestiti, C ++ nativi, servizi Web C # e programmi di installazione.

Forse qualcosa è rotto, ma poiché questo è l'unico modo in cui lavoro, non me ne accorgo.

Sarebbe interessante scoprire cosa sto rompendo.


0

Di solito, è necessario copiare Local solo se si desidera che il progetto utilizzi la DLL presente nel proprio cestino rispetto a ciò che si trova altrove (GAC, altri progetti, ecc.)

Tenderei ad essere d'accordo con gli altri sul fatto che dovresti anche provare, se possibile, a infrangere quella soluzione.

Puoi anche utilizzare Configuration Manager per creare configurazioni di build diverse all'interno di quella soluzione che costruirà solo determinati set di progetti.

Sembrerebbe strano se tutti e 100 i progetti si basassero l'uno sull'altro, quindi dovresti essere in grado di suddividerlo o utilizzare Configuration Manager per aiutarti.


0

Puoi avere i riferimenti dei tuoi progetti che puntano alle versioni di debug delle dll. Rispetto allo script msbuild, è possibile impostare il /p:Configuration=Release, quindi si avrà una versione di rilascio della propria applicazione e di tutti gli assembly satellite.


1
Bruno - sì, questo funziona con i riferimenti di progetto, che è uno dei motivi per cui siamo finiti con una soluzione di progetto 100 in primo luogo. Non funziona sui riferimenti in cui sfoglio le versioni di debug predefinite - Mi ritrovo con un'app di rilascio costruita contro gli assembly di debug, il che è un problema
Dave Moore,

4
Modifica il tuo file di progetto in un editor di testo e usa $ (Configuration) in HintPath, ad es. <HintPath> .. \ output \ $ (Configuration) \ test.dll </HintPath>.
ultravelocità


0

Se il riferimento non è contenuto all'interno del GAC, dobbiamo impostare Copia locale su true in modo che l'applicazione funzioni, se siamo sicuri che il riferimento sarà preinstallato nel GAC, allora può essere impostato su false.


0

Bene, certamente non so come risolvono i problemi, ma ho avuto contatti con una soluzione di build che si è dimostrata utile in modo tale che tutti i file creati fossero messi su un ramdisk con l'aiuto di collegamenti simbolici .

  • c: \ cartella soluzione \ bin -> ramdisk r: \ cartella soluzione \ bin \
  • c: \ cartella soluzione \ obj -> ramdisk r: \ cartella soluzione \ obj \

  • Puoi anche dire allo studio visivo quale directory temporanea può usare per la compilazione.

In realtà non è stato tutto quello che ha fatto. Ma ha davvero colpito la mia comprensione delle prestazioni.
100% di utilizzo del processore e un grande progetto in meno di 3 minuti con tutte le dipendenze.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.