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

Posted

技术标签:

【中文标题】在 MSBuild 中确定 ProjectReference 的输出而不触发冗余重建【英文标题】:Determining outputs of a ProjectReference in MSBuild without triggering redundant rebuilds 【发布时间】:2011-01-20 12:02:38 【问题描述】:

作为包含许多项目的解决方案的一部分,我有一个引用的项目(通过<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" />

但是,我遇到了这个问题。 &lt;MSBuild 步骤的IncrementalClean 任务最终删除了ProjectC 的一些输出。在 VS2008 下运行此程序时,build.force 文件被存放在 ProjectC 的 obj/Debug 文件夹中,如果项目包含此 AfterBuild 目标,则如果我在整个解决方案上执行构建,则触发 ProjectC 重建,而如果一个排除这个项目从构建中,它[正确地] 不会触发 ProjectC 的重建(并且关键会重建 ProjectC 的所有依赖项)。在这种情况下,这可能是特定于 VS 的诡计,不会在 TeamBuild 或其他命令行 MSBuild 调用的上下文中发生(但最常见的用法是通过 VS,因此我需要以任何一种方式解决此问题)

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

虽然我完全有可能在做一些愚蠢的事情并且有人会立即指出它是什么(这很好),但请放心,我已经花了很多时间研究/v:diag 输出等(虽然我没有试图从头开始构建复制品 - 这是在相对复杂的整体构建的背景下)

【问题讨论】:

【参考方案1】:

正如我在评论中指出的,对引用的项目调用 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>

【讨论】:

【参考方案2】:

您的原始解决方案只需更改即可工作

Targets="Build"

Targets="GetTargetPath"

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

【讨论】:

+1 谢谢,无法验证它是否确实有效(因为桥下有很多水!),但似乎是一种非常可行且干净的方法。 YMMV:GetTargetPath 仅返回项目的主要输出程序集 (.DLL/.EXE) 如果您想要该项目的 所有 引用程序集,则需要不同的技术跨度> 如果其他人需要为 C++ 项目执行此操作,则要调用的正确目标是 Targets="GetNativeTargetPath"。【参考方案3】:

如果您首先调用这样的目标,则可以保护您在 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>

【讨论】:

首先,感谢/祝贺您选择这款益智游戏作为您的第一个 SO 答案!我个人对引入对这种性质的 MSBuild 内部的依赖保持沉默,但是是的,这肯定会让我以编程方式实现“它写了什么文件”! SO上还有一些其他“计算输出”问题,这可能更适合作为答案。对我来说,这里的主要问题是 &lt;MSBuild 位仍在弄乱它自己的依赖关系,并且错误地导致在我的第一个构建之后直接构建它刚刚构建的东西。【参考方案4】:

我的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 下中断(所有输出最终都在一个目录中),并且如果相关项目的任何输出的名称发生更改。

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

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

【讨论】:

以上是关于在 MSBuild 中确定 ProjectReference 的输出而不触发冗余重建的主要内容,如果未能解决你的问题,请参考以下文章

MSBuild 2017无法构建Cordova解决方案

MSBuild 内置变量列表

MSBuild 默默地跳过一个项目(在我的 sln 中的许多项目中)

如何使用 MSBuild 引用不同版本的 dll

如何在批处理文件中调用设置为“C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe”的变量?

Docs-VisualStudio-MSBuild-MSBuild参考:常用的 MSBuild 项目项