Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MSBuild to copy dynamically generated files as part of project dependency

I have a custom msbuild task that is generating some output files to the output directory ($(TargetDir)) of a ProjectA. Current code is something like this:

<MyCustomTask ...>
   <Output TaskParameter="OutputFiles" ItemName="FileWrites"/>
</MyCustomTask>

A ProjectB is referencing ProjectA but the problem is that when building ProjectB, generated files by MyCustomTask are not copied to the output directory of the ProjectB.

How can we get dynamically generated additional files to be copied as part of project dependency with MSBuild?

like image 568
xoofx Avatar asked Jan 14 '13 16:01

xoofx


4 Answers

I have finally managed to perform automatically the copy from Project B without having to modify it. IIya was not so far from the solution, but the fact is that I cannot generate statically as the list of files to generate from Project A with MyCustomTask is dynamic. After digging more into Microsoft.Common.targets, I have found that ProjectB will get the list of output from Project A by calling the target GetCopyToOutputDirectoryItems. This target is dependent from AssignTargetPaths which itself is dependent on the target list property AssignTargetPathsDependsOn.

So in order to generate dynamically content and to get this content being copied automatically through standard project dependency, we need to hook Project A at two different places:

  • In AssignTargetPathsDependsOn as it is called indirectly by Project B on Project A through GetCopyToOutputDirectoryItems. And also it is indirectly called by Project A when PrepareResource is called. Here, we are just outputing the list of files that will be generated (by Project A) or consumed by Project B. AssignTargetPathsDependsOn will call a custom task MyCustomTaskList which is only responsible to output the list of files (but not to generate them), this list of files will create dynamic "Content" with CopyOutputDirectory.
  • In BuildDependsOn in order to actually generate the content in Project A. This will call MyCustomTask that will generate the content.

All of this was setup like this in ProjectA:

<!-- In Project A -->

<!-- Task to generate the files -->
<UsingTask TaskName="MyCustomTask" AssemblyFile="$(PathToMyCustomTaskAssembly)"/>

<!-- Task to output the list of generated of files - It doesn't generate the file -->
<UsingTask TaskName="MyCustomTaskList" AssemblyFile="$(PathToMyCustomTaskAssembly)"/>

<!-- 1st PART : When Project A is built, It will generate effectively the files -->
<PropertyGroup>
  <BuildDependsOn>
    MyCustomTaskTarget;
    $(BuildDependsOn);
  </BuildDependsOn>
</PropertyGroup>

<Target Name="MyCustomTaskTarget">
  <!-- Call MyCustomTask generate the files files that will be generated by MyCustomTask -->
  <MyCustomTask
      ProjectDirectory="$(ProjectDir)"
      IntermediateDirectory="$(IntermediateOutputPath)"
      Files="@(MyCustomFiles)"
      RootNamespace="$(RootNamespace)"
      >
  </MyCustomTask>
</Target>

<!-- 2nd PART : When Project B is built, It will call GetCopyToOutputDirectoryItems on ProjectA so we need to generate this list when it is called  -->
<!-- For this we need to override AssignTargetPathsDependsOn in order to generate the list of files -->
<!-- as GetCopyToOutputDirectoryItems  ultimately depends on AssignTargetPathsDependsOn -->
<!-- Content need to be generated before AssignTargets, because AssignTargets will prepare all files to be copied later by GetCopyToOutputDirectoryItems -->
<!-- This part is also called from ProjectA when target 'PrepareResources' is called -->
<PropertyGroup>
  <AssignTargetPathsDependsOn>
    $(AssignTargetPathsDependsOn);
    MyCustomTaskListTarget;
  </AssignTargetPathsDependsOn>
</PropertyGroup>

<Target Name="MyCustomTaskListTarget">

  <!-- Call MyCustomTaskList generating the list of files that will be generated by MyCustomTask -->
  <MyCustomTaskList
      ProjectDirectory="$(ProjectDir)"
      IntermediateDirectory="$(IntermediateOutputPath)"
      Files="@(MyCustomFiles)"
      RootNamespace="$(RootNamespace)"
      >
      <Output TaskParameter="ContentFiles" ItemName="MyCustomContent"/>
  </MyCustomTaskList>

  <ItemGroup>
    <!--Generate the lsit of content generated by MyCustomTask -->
    <Content Include="@(MyCustomContent)" KeepMetadata="Link;CopyToOutputDirectory"/>
  </ItemGroup>
</Target>

This method is working with anykind of C# projects that is using Common.Targets (so It is working with pure Desktop, WinRT XAML App or Windows Phone 8 projects).

like image 108
xoofx Avatar answered Nov 02 '22 14:11

xoofx


Something like this seems to work, either include it manually into ProjectA's .csproj (keep in mind VS has a bad habit of occasionally resolving wildcards into absolute paths and overwriting .csproj) or inject dynamically by the custom task itself. Also, VS caches itemgroups on open, so it might not copy the files or fail the build if they were there but deleted. In that case projects need to be reloaded or VS restarted for itemgroups to be reevaluated. MSBuild, TFS, etc should always work.

<ItemGroup>
  <Content Include="$(TargetDir)\*.txt">
    <Link>%(Filename)%(Extension)</Link>
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </Content>
</ItemGroup>
like image 29
Ilya Kozhevnikov Avatar answered Nov 02 '22 12:11

Ilya Kozhevnikov


If you already doing this build yourself with MSBuild, could you add a Copy Task to push the files around yourself?

like image 36
Wolfwyrd Avatar answered Nov 02 '22 12:11

Wolfwyrd


For what it's worth, if I placed a <None Link="..." /> within a target, I'm able to get the output processed without it showing up in my SDK based project. Additionally, other projects that reference this project gets this as part of the output.

E.g.

    <ItemGroup>
        <WebPackBuildOutput Include="..\..\WebPackOutput\dist\**\*" />
    </ItemGroup>

    <Target Name="WebPackOutputContentTarget" BeforeTargets="BeforeBuild">
        <Message Text="Output dynamic content: @(WebPackBuildOutput)" Importance="high"/>
        <ItemGroup>
            <!-- Manually constructing Link metadata, works in classic projects as well -->
            <None Include="@(WebPackBuildOutput)" Link="dist\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
        </ItemGroup>        
    </Target>
like image 39
Jaans Avatar answered Nov 02 '22 14:11

Jaans