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?
Solution 1:
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 whenPrepareResource
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 taskMyCustomTaskList
which is only responsible to output the list of files (but not to generate them), this list of files will create dynamic "Content" withCopyOutputDirectory
. - In
BuildDependsOn
in order to actually generate the content in Project A. This will callMyCustomTask
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).
Solution 2:
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>
Solution 3:
If you already doing this build yourself with MSBuild, could you add a Copy Task to push the files around yourself?