tags:

views:

894

answers:

1

I have an MSBuild file and I am building C# projects like this:

<ItemGroup>
    <ProjectsToBuild Include="./source/ProjectA/ProjectA.csproj"/>
    <ProjectsToBuild Include="./source/ProjectB/ProjectB.csproj"/>
</ItemGroup>

<Target Name="Build">
    <MSBuild Projects="@(ProjectsToBuild)" Targets="Build">
        <Output ItemName="ProjectOutputs" TaskParameter="TargetOutputs"/>
    </MSBuild>
    <Message Text="@ProjectOutputs"/>
</Target>

I successfully get an Item containing all of the .dll files that were built:

Build:
    c:\code\bin\ProjectA.dll;c:\code\bin\ProjectB.dll

I would also like to get the Content item from each project without modifying the .csproj files. After digging around in the Microsoft .targets files, I was almost able to get it working with this:

<MSBuild Projects="@(ProjectsToBuild)" Targets="ContentFilesProjectOutputGroup">
    <Output ItemName="ContentFiles" TaskParameter="TargetOutputs"/>
</MSBuild>
<Message Text="@(ContentFiles->'%(RelativeDir)')"/>

The problem with this approach is the RelativeDir is not being set correctly. I am getting the full path instead of relative:

Build:
    c:\ProjectA\MyFolder\MyControl.ascx;c:\ProjectB\MyOtherFolder\MyCSS.css;

instead of:

Build:
    MyFolder\MyControl.ascx;MyOtherFolder\MyCSS.css;

Is there a property I can pass to the MSBuild task that will make RelativeDir behave correctly?

Or, even better, is there an easier way to get the Content item?

+3  A: 

Hi,

You can do this but it is not very intutive. I've discussed this type of technique a few times on my blog ( which is currently down :( ).

So create a new file, I named it GetContentFiles.proj which is shown here.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"&gt;

  <ItemGroup>
    <Projects Include="WindowsFormsApplication1\WindowsFormsApplication1.csproj"/>
  </ItemGroup>

  <!-- This target will be executed once for each file declared in the Project target -->
  <Target Name="PrintFiles" Outputs="%(Projects.Identity)">

    <Message Text="PrintFiles" Importance="high"/>

    <MSBuild Projects="$(MSBuildProjectFile)"
             Targets="GetContentFiles"
             Properties="ProjectToGetFiles=%(Projects.Identity)">
      <Output ItemName="projContent" TaskParameter="TargetOutputs"/>
    </MSBuild>

    <Message Text="ProjContent: @(projContent)" Importance="high"/>

    <!-- Transform the projContent to have correct path -->

    <!-- 
    Get the relative path to the project itself, this serves as the base for
    the Content files path
    -->
    <PropertyGroup>
      <_ProjRelativeDir>%(Projects.RelativeDir)</_ProjRelativeDir>
    </PropertyGroup>

    <!-- This item will contain the item with the corrected path values -->
    <ItemGroup>
      <ProjContentFixed Include="@(projContent->'$(_ProjRelativeDir)%(RelativeDir)%(Filename)%(Extension)')"/>
    </ItemGroup>

    <!-- Create a new item with the correct relative dirs-->
    <Error Condition="!Exists('%(ProjContentFixed.FullPath)')"
           Text="File not found at [%(ProjContentFixed.FullPath)]"/>
  </Target>

  <Import Project="$(ProjectToGetFiles)" Condition="'$(ProjectToGetFiles)'!=''"/>

  <Target Name="GetContentFiles" Condition="'$(ProjectToGetFiles)'!=''" Outputs="@(Content)">
    <Message Text="Content : @(Content)" Importance="high"/>
    <Message Text="Inside GetContentFiles" Importance="high"/>    
  </Target>

</Project>

I will try and explain this, but it may be tough to follow. Let me know if you need me to expand on it. This file has two targets PrintFiles and GetContentFiles. The entry point into this file is the PrintFiles target, in the sense that this is the target that you are going to call. So you call the PrintFiles target which it then uses the MSBuild task to call the GetContentFiles target on itself, also it passes a value for the ProjectToGetFiles property. Because of that the Import elemnent will be executed. So what you are really doing is taking the project defined in the ProjectToGetFiles property and extending it to include the target GetContentFiles (and whatever other content is inside the GetContentFiles.proj file). So we are effectively extending that file. I'm calling this technique "MSBuild Inheritance" because. So inside the GetContentFiles target we can access all properties and items that are declared inthe ProjectToGetFiles property. So I take advantage of that by simply putting the content of the Content item into the outputs for the target, which can be accessed by the original file using the TargetOutputs from the MSBuild task.

You mentioned in your post that you wanted to correct the path values to be the right ones. The problem here is that in the .csproj file all items are declared relative to the original project file. So if you "extend" the project file in this way from a file in a different directory you must correct the file path values manually. I've done this inside the PrintFiles target, check it out.

If you execute the command msbuild GetContentFile.proj /fl /t:PrintFiles the result would be:

Build started 7/3/2009 12:56:35 AM.
Project "C:\Data\Development\My Code\Community\MSBuild\FileWrites\GetContentFile.proj" on node 0 (PrintFiles target(s)).
  PrintFiles
Project "C:\Data\Development\My Code\Community\MSBuild\FileWrites\GetContentFile.proj" (1) is building "C:\Data\Development\My Co
de\Community\MSBuild\FileWrites\GetContentFile.proj" (1:2) on node 0 (GetContentFiles target(s)).
  Content : Configs\Config1.xml;Configs\Config2.xml
  Inside GetContentFiles
Done Building Project "C:\Data\Development\My Code\Community\MSBuild\FileWrites\GetContentFile.proj" (GetContentFiles target(s)).

PrintFiles:
  ProjContent: Configs\Config1.xml;Configs\Config2.xml
Done Building Project "C:\Data\Development\My Code\Community\MSBuild\FileWrites\GetContentFile.proj" (PrintFiles target(s)).


Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.03
Sayed Ibrahim Hashimi
Thank you! The explanation of "MSBuild Inheritance" helps a lot. I'm streamlining our build process and that will let me do so much more without touching any individual .csproj files.What is the significance of the underscore in _projRelativeDir?I do have your book and I love it! I've been working with Properties and Items for a while, but never quite understood them. Your explanation went into a lot of detail on the parts most articles assume people already know.
Michael
Hi, thanks for taking interest in my book. The leading underscore tells other people "don't touch this" similar to a private property. If you look @ Microsoft.Common.targets you will see a bunch of these, so don't change or rely on those.
Sayed Ibrahim Hashimi