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

发布于 2024-08-22 23:09:51 字数 1389 浏览 14 评论 0 原文

作为包含许多项目的解决方案的一部分,我有一个引用的项目(通过 解决方案中的其他三个项目,以及其他一些项目)。在 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" />

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

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

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

As part of a solution containing many projects, I have a project that references (via a <ProjectReference> three other projects in the solution, plus some others). In the AfterBuild, I need to copy the outputs of 3 specific dependent projects to another location.

Via various SO answers, etc. the way I settled on to accomplish that was:

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

However, I ran into problems with this. The <MSBuild step's IncrementalClean task ends up deleting a number of the outputs of ProjectC. When running this under VS2008, a build.force file being deposited in the obj/Debug folder of ProjectC which then triggers ProjectC getting rebuilt if I do a Build on the entire solution if the project containing this AfterBuild target, whereas if one excludes this project from the build, it [correctly] doesn't trigger a rebuild of ProjectC (and critically a rebuild of all dependents of ProjectC). This may be VS-specific trickery in this case which would not occur in the context of a TeamBuild or other commandline MSBuild invocation (but the most common usage will be via VS so I need to resolve this either way)

The dependent projects (and the rest of the solution in general) have all been created interactively with VS, and hence the ProjectRefences contain relative paths etc. I've seen mention of this being likely to causing issues - but without a full explanation of why, or when it'll be fixed or how to work around it. In other words, I'm not really interested in e.g. converting the ProjectReference paths to absolute paths by hand-editing the .csproj.

While it's entirely possible I'm doing something stupid and someone will immediately point out what it is (which would be great), be assured I've spent lots of time poring over /v:diag outputs etc. (although I havent tried to build a repro from the ground up - this is in the context of a relatively complex overall build)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

蓝眼睛不忧郁 2024-08-29 23:09:51

正如我的评论中所述,在引用的项目上调用 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>

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>
沫离伤花 2024-08-29 23:09:51

您原来的解决方案只需更改

Targets="Build"

Targets="GetTargetPath"

即可工作。 GetTargetPath 目标仅返回 TargetPath 属性,不需要构建。

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.

混吃等死 2024-08-29 23:09:51

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

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>
寒尘 2024-08-29 23:09:51

我的 当前解决方法基于这个SO问题,即,我有:

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

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>
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文