Determining outputs of a ProjectReference in MSBuild without triggering redundant rebuilds

大兔子大兔子 提交于 2019-11-29 10:23:16

As noted in my comment, calling GetTargetPath on the referenced project only returns the Primary output assembly of that project. To get all the referenced copy-local assemblies of the referenced project it's a bit messier.

Add the following to each project that you are referencing that you want to get the CopyLocals of:

    <Target
    Name="ComputeCopyLocalAssemblies"
    DependsOnTargets="ResolveProjectReferences;ResolveAssemblyReferences"
    Returns="@(ReferenceCopyLocalPaths)" /> 

My particular situation is that I needed to recreate the Pipeline folder structure for System.AddIn in the bin folder of my top level Host project. This is kinda messy and I was not happy with the MSDN suggested solutions of mucking with the OutputPath - as that breaks on our build server and prevents creating the folder structure in a different project (eg a SystemTest)

So along with adding the above target (using a .targets import), I added the following to a .targets file imported by each "host" that needs the pipeline folder created:

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

I also needed to add the required PipelineFolder meta data to the actual project references. For example:

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

Your original solution should work simply by changing

Targets="Build"

to

Targets="GetTargetPath"

The GetTargetPath target simply returns the TargetPath property and doesn't require building.

You may protect your files in ProjectC if you call a target like this first:

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

My current workaround is based on this SO question, i.e, I have:

    <ItemGroup>
        <DependentAssemblies Include="
            ..\ProjectA\bin\$(Configuration)\ProjectA.dll;
            ..\ProjectB\bin\$(Configuration)\ProjectB.dll;
            ..\ProjectC\bin\$(Configuration)\ProjectC.dll">
        </DependentAssemblies>
    </ItemGroup>

This however will break under TeamBuild (where all the outputs end up in one directory), and also if the names of any of the outputs of the dependent projects change.

EDIT: Also looking for any comments on whether there's a cleaner answer for how to make the hardcoding slightly cleaner than:

    <PropertyGroup>
        <_TeamBuildingToSingleOutDir Condition="'$(TeamBuildOutDir)'!='' AND '$(CustomizableOutDir)'!='true'">true</_TeamBuildingToSingleOutDir>
    </PropertyGroup>

and:

    <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>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!