确定MSBuild中ProjectReference的输出而不触发冗余重建

时间:2010-02-24 11:36:20

标签: msbuild msbuild-task project-reference

作为包含许多项目的解决方案的一部分,我有一个项目引用(通过<ProjectReference>解决方案中的其他三个项目,以及其他一些项目。在AfterBuild中,我需要将3个特定依赖项目的输出复制到另一个位置。

通过各种SO答案等,我决定实现这一目标的方式是:

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

然而,我遇到了这个问题。 <MSBuild步骤的IncrementalClean任务最终会删除ProjectC的多个输出。在VS2008下运行时,build.force文件存放在ProjectC的obj/Debug文件夹中,如果我对整个解决方案进行构建,如果包含此AfterBuild的项目,则会触发ProjectC重建目标,如果从构建中排除此项目,它[正确]不会触发ProjectC的重建(并且批判性重建ProjectC的所有依赖项)。在这种情况下,这可能是VS特定的技巧,这不会发生在TeamBuild或其他命令行MSBuild调用的上下文中(但最常见的用法是通过VS,所以我需要以这两种方式解决这个问题)

依赖项目(以及一般解决方案的其余部分)都是与VS交互式创建的,因此ProjectRefence包含相对路径等。我看到提到这可能会导致问题 - 但没有完整解释原因,或何时修复或如何解决它。换句话说,我对此并不感兴趣。通过手动编辑.csproj将ProjectReference路径转换为绝对路径。

虽然我完全有可能做一些愚蠢的事情,有人会立即指出它是什么(这将是伟大的),请放心,我花了很多时间仔细研究/v:diag输出等(虽然我没有尝试从头开始构建一个repro - 这是在相对复杂的整体构建的背景下)

4 个答案:

答案 0 :(得分:7)

如我的评论中所述,在引用的项目上调用GetTargetPath只返回该项目的主输出程序集。要获取引用项目的所有引用的复制本地程序集,它会有点麻烦。

将以下内容添加到您要引用的每个项目中,以获取CopyLocals:

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

我的特殊情况是我需要在我的顶级主机项目的bin文件夹中重新创建System.AddIn的Pipeline文件夹结构。这有点凌乱,我对MSDN建议的与OutputPath混淆的解决方案不满意 - 因为它在我们的构建服务器上中断并阻止在不同的项目中创建文件夹结构(例如SystemTest)

因此,除了添加上述目标(使用.targets导入)之外,我还将以下内容添加到需要创建管道文件夹的每个“主机”导入的.targets文件中:

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

我还需要将所需的PipelineFolder元数据添加到实际的项目引用中。例如:

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

答案 1 :(得分:6)

原始解决方案应该只需更改

即可
Targets="Build"

Targets="GetTargetPath"

GetTargetPath目标只返回TargetPath属性,不需要构建。

答案 2 :(得分:2)

如果你先调用这样的目标,你可以在ProjectC中保护你的文件:

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

答案 3 :(得分:1)

current workaround is based on this SO question,即我有:

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

然而,这将在TeamBuild(其中所有输出最终都在一个目录中)以及依赖项目的任何输出的名称发生变化时中断。

编辑:还在寻找关于如何使硬编码比以下更清晰的答案更清晰的评论:

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

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