Détermination des sorties d'un ProjectReference dans MSBuild sans déclencher reconstructions redondants
-
22-09-2019 - |
Question
Dans le cadre d'une solution contenant de nombreux projets, j'ai un projet que les références (via un <ProjectReference>
trois autres projets dans la solution, ainsi que quelques autres). Dans le AfterBuild
, je dois copier les résultats de 3 projets dépendants spécifiques à un autre endroit.
Via diverses réponses SO, etc. la façon dont je me suis installé à accomplir qui était:
<MSBuild
Projects="@(ProjectReference)"
Targets="Build"
BuildInParallel="true"
Condition="'%(Name)'=='ProjectA' OR '%(Name)'=='ProjectB' OR '%(Name)'=='ProjectC'">
<Output TaskParameter="TargetOutputs" ItemName="DependentAssemblies" />
</MSBuild>
<Copy SourceFiles="@(DependentAssemblies)" DestinationFolder="XX" SkipUnchangedFiles="true" />
Cependant, je suis tombé sur des problèmes avec cela. La tâche de <MSBuild
étape de IncrementalClean
finit par la suppression d'un certain nombre de sorties de ProjectC
. Lors de l'exécution de cette sous VS2008, un fichier build.force
étant déposé dans le dossier obj/Debug
de ProjectC qui déclenche alors ProjectC se reconstruit si je compilons sur toute solution si le projet contenant cet objectif de AfterBuild
, alors que si l'on exclut ce projet de la construction, il [correctement] ne déclenche pas une recréation de ProjectC (et critique une reconstruction de toutes les personnes à charge de ProjectC). Cela peut être supercherie VS spécifique dans ce cas qui ne se produira pas dans le contexte d'un TeamBuild ou une autre invocation commandline MSBuild (mais sera par VS l'usage le plus commun, alors je dois résoudre ce soit ainsi)
Les projets dépendants (et le reste de la solution en général) ont tous été créés de manière interactive avec VS, et donc les ProjectRefence
s contiennent des chemins relatifs, etc. Je l'ai vu mention de ce qui est susceptible de causer des problèmes - mais sans plein explication des raisons pour lesquelles, ou quand il sera fixé ou comment travailler autour d'elle. En d'autres termes, je ne suis pas vraiment intéressé par exemple convertir les chemins de ProjectReference
à des chemins absolus par la main modification du .csproj.
Bien qu'il soit tout à fait possible que je fais quelque chose de stupide et quelqu'un va immédiatement signaler ce qu'il est (ce qui serait formidable), soyez assurés que j'ai passé beaucoup de temps penché sur les sorties /v:diag
etc. (bien que je n'ai pas essayé de construire une repro à partir du sol - ce qui est dans le contexte d'une construction globale relativement complexe)
La solution
Comme indiqué dans mon commentaire, appelant GetTargetPath sur le projet référencé renvoie uniquement l'ensemble de sortie primaire de ce projet. Pour obtenir tous les ensembles de la copie locale référencée du projet référencé, il est un peu messier.
Ajoutez ce qui suit à chaque projet que vous faites référence que vous souhaitez obtenir les CopyLocals de:
<Target
Name="ComputeCopyLocalAssemblies"
DependsOnTargets="ResolveProjectReferences;ResolveAssemblyReferences"
Returns="@(ReferenceCopyLocalPaths)" />
Ma situation est que je devais recréer la structure du dossier Pipeline pour System.AddIn dans le dossier bin de mon projet de haut niveau de l'hôte. C'est un peu en désordre et je n'étais pas satisfait de la MSDN propose des solutions de déblayage avec le OutputPath - comme qui casse sur notre serveur de build et empêche la création de la structure du dossier dans un autre projet (par exemple un SystemTest)
ainsi que l'ajout de la cible ci-dessus (à l'aide d'une importation .targets), j'ajouté ce qui suit dans un fichier .targets importé par chaque « hôte » qui a besoin du dossier de pipeline créé:
<Target
Name="ComputePipelineAssemblies"
BeforeTargets="_CopyFilesMarkedCopyLocal"
Outputs="%(ProjectReference.Identity)">
<ItemGroup>
<_PrimaryAssembly Remove="@(_PrimaryAssembly)" />
<_DependentAssemblies Remove="@(_DependentAssemblies)" />
</ItemGroup>
<!--The Primary Output of the Pipeline project-->
<MSBuild Projects="%(ProjectReference.Identity)"
Targets="GetTargetPath"
Properties="Configuration=$(Configuration)"
Condition=" '%(ProjectReference.PipelineFolder)' != '' ">
<Output TaskParameter="TargetOutputs"
ItemName="_PrimaryAssembly" />
</MSBuild>
<!--Output of any Referenced Projects-->
<MSBuild Projects="%(ProjectReference.Identity)"
Targets="ComputeCopyLocalAssemblies"
Properties="Configuration=$(Configuration)"
Condition=" '%(ProjectReference.PipelineFolder)' != '' ">
<Output TaskParameter="TargetOutputs"
ItemName="_DependentAssemblies" />
</MSBuild>
<ItemGroup>
<ReferenceCopyLocalPaths Include="@(_PrimaryAssembly)"
Condition=" '%(ProjectReference.PipelineFolder)' != '' ">
<DestinationSubDirectory>%(ProjectReference.PipelineFolder)</DestinationSubDirectory>
</ReferenceCopyLocalPaths>
<ReferenceCopyLocalPaths Include="@(_DependentAssemblies)"
Condition=" '%(ProjectReference.PipelineFolder)' != '' ">
<DestinationSubDirectory>%(ProjectReference.PipelineFolder)</DestinationSubDirectory>
</ReferenceCopyLocalPaths>
</ItemGroup>
</Target>
J'ai aussi besoin d'ajouter le PipelineFolder nécessaire des méta-données aux références réelles du projet. Par exemple:
<ProjectReference Include="..\Dogs.Pipeline.AddInSideAdapter\Dogs.Pipeline.AddInSideAdapter.csproj">
<Project>{FFCD0BFC-5A7B-4E13-9E1B-8D01E86975EA}</Project>
<Name>Dogs.Pipeline.AddInSideAdapter</Name>
<Private>False</Private>
<PipelineFolder>Pipeline\AddInSideAdapter\</PipelineFolder>
</ProjectReference>
Autres conseils
Votre solution originale devrait fonctionner en changeant simplement
Targets="Build"
à
Targets="GetTargetPath"
La cible GetTargetPath
retourne simplement la propriété TargetPath
et ne nécessite pas la construction.
Vous pouvez protéger vos fichiers ProjectC si vous appelez une cible comme cette première:
<Target Name="ProtectFiles">
<ReadLinesFromFile File="obj\ProjectC.csproj.FileListAbsolute.txt">
<Output TaskParameter="Lines" ItemName="_FileList"/>
</ReadLinesFromFile>
<CreateItem Include="@(_DllFileList)" Exclude="File1.sample; File2.sample">
<Output TaskParameter="Include" ItemName="_FileListWitoutProtectedFiles"/>
</CreateItem>
<WriteLinesToFile
File="obj\ProjectC.csproj.FileListAbsolute.txt"
Lines="@(_FileListWitoutProtectedFiles)"
Overwrite="true"/>
</Target>
Mon solution actuelle est basée sur cette question SO , à savoir, je:
<ItemGroup>
<DependentAssemblies Include="
..\ProjectA\bin\$(Configuration)\ProjectA.dll;
..\ProjectB\bin\$(Configuration)\ProjectB.dll;
..\ProjectC\bin\$(Configuration)\ProjectC.dll">
</DependentAssemblies>
</ItemGroup>
Cependant, cela va se rompre sous TeamBuild (où toutes les sorties se retrouvent dans un répertoire), et même si le nom de l'un des résultats des projets dépendants changent.
EDIT: Aussi la recherche des commentaires sur s'il y a une réponse plus propre pour savoir comment faire le hardcoding un peu plus propre que:
<PropertyGroup>
<_TeamBuildingToSingleOutDir Condition="'$(TeamBuildOutDir)'!='' AND '$(CustomizableOutDir)'!='true'">true</_TeamBuildingToSingleOutDir>
</PropertyGroup>
et
<ItemGroup>
<DependentAssemblies
Condition="'$(_TeamBuildingToSingleOutDir)'!='true'"
Include="
..\ProjectA\bin\$(Configuration)\ProjectA.dll;
..\ProjectB\bin\$(Configuration)\ProjectB.dll;
..\ProjectC\bin\$(Configuration)\ProjectC.dll">
</DependentAssemblies>
<DependentAssemblies
Condition="'$(_TeamBuildingToSingleOutDir)'=='true'"
Include="
$(OutDir)\ProjectA.dll;
$(OutDir)\ProjectB.dll;
$(OutDir)\ProjectC.dll">
</DependentAssemblies>
</ItemGroup>