Determinação de saídas de uma projectreferência no msbuild sem desencadear reconstruções redundantes
-
22-09-2019 - |
Pergunta
Como parte de uma solução contendo muitos projetos, tenho um projeto que referências (via A <ProjectReference>
Três outros projetos na solução, além de outros). No AfterBuild
, Preciso copiar as saídas de 3 projetos dependentes específicos para outro local.
Por meio de várias respostas, etc. A maneira como eu decidi a realizar isso foi:
<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" />
No entanto, tive problemas com isso. o <MSBuild
degraus IncrementalClean
tarefa acaba excluindo várias das saídas de ProjectC
. Ao executar isso em vs2008, um build.force
arquivo sendo depositado no obj/Debug
Pasta do Projectc, que aciona o Projectc sendo reconstruído se eu fizer uma construção em toda a solução se o projeto que contém isso AfterBuild
Target, enquanto que se alguém excluir este projeto da construção, ele [corretamente] não desencadeia uma reconstrução do ProjectC (e criticamente uma reconstrução de todos os dependentes do ProjectC). Isso pode ser truque específico do VS neste caso, o que não ocorreria no contexto de um Teambuild ou outro comando MSBuild Invocation (mas o uso mais comum será via VS, então preciso resolver isso de qualquer maneira)
Os projetos dependentes (e o restante da solução em geral) foram todos criados interativamente com vs e, portanto, o ProjectRefence
s contêm caminhos relativos etc. Eu vi mencionar que isso provável de causar problemas - mas sem uma explicação completa do porquê, ou quando será corrigida ou como contorná -lo. Em outras palavras, não estou realmente interessado em converter o ProjectReference
Caminhos para caminhos absolutos, editando manualmente o .csproj.
Embora seja perfeitamente possível, estou fazendo algo estúpido e alguém imediatamente apontará o que é (o que seria ótimo), tenha certeza de que gastei muito tempo se debatendo /v:diag
saídas etc. (embora eu não tenha tentado construir uma repro desde o início - isso está no contexto de uma construção geral relativamente complexa)
Solução
Conforme observado no meu comentário, chamando o GetTargetPath no projeto referenciado retorna apenas a montagem de saída primária desse projeto. Para obter todos os conjuntos de cópias referenciados do projeto referenciado, é um pouco mais confuso.
Adicione o seguinte a cada projeto que você está referenciando que deseja obter os copilocais de:
<Target
Name="ComputeCopyLocalAssemblies"
DependsOnTargets="ResolveProjectReferences;ResolveAssemblyReferences"
Returns="@(ReferenceCopyLocalPaths)" />
Minha situação específica é que eu precisava recriar a estrutura da pasta do pipeline para o sistema.Addin na pasta BIN do meu projeto de host de nível superior. Isso é meio bagunçado e eu não fiquei feliz com o MSDN sugerido soluções de mexer com o outputPath - pois isso quebra em nosso servidor de construção e evita a criação da estrutura da pasta em um projeto diferente (por exemplo, um sistema de sistema)
Assim, além de adicionar o destino acima (usando um .Targets Import), adicionei o seguinte a um arquivo .Targets importado por cada "host" que precisa da pasta de pipeline criada:
<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>
Eu também precisava adicionar os meta dados necessários do PipeLEFder às referências reais do projeto. Por exemplo:
<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>
Outras dicas
Sua solução original deve funcionar simplesmente mudando
Targets="Build"
para
Targets="GetTargetPath"
o GetTargetPath
alvo simplesmente retorna o TargetPath
propriedade e não requer construção.
Você pode proteger seus arquivos no ProjectC se ligar para um alvo como este primeiro:
<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>
Meu A solução alternativa atual é baseada nisso, então pergunta, ou seja, eu tenho:
<ItemGroup>
<DependentAssemblies Include="
..\ProjectA\bin\$(Configuration)\ProjectA.dll;
..\ProjectB\bin\$(Configuration)\ProjectB.dll;
..\ProjectC\bin\$(Configuration)\ProjectC.dll">
</DependentAssemblies>
</ItemGroup>
No entanto, isso será dividido no TeamBuild (onde todas as saídas acabam em um diretório) e também se os nomes de qualquer uma das saídas dos projetos dependentes mudarem.
EDIT: Também procurando comentários sobre se há uma resposta mais limpa de como tornar o codificação um pouco mais limpa do que:
<PropertyGroup>
<_TeamBuildingToSingleOutDir Condition="'$(TeamBuildOutDir)'!='' AND '$(CustomizableOutDir)'!='true'">true</_TeamBuildingToSingleOutDir>
</PropertyGroup>
e:
<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>