/ MSBuild

MSBuild Part 3: Get Organized

Last time, we learned a lot about targets and we also started to look at properties. This time we will look at some more constructs that can be useful to know when you are creating MSBuild scripts. We will also go through how we can control logging verbosity and finally we will look at how we can organize our project and make it more manageable. So let us get started!

This post is part of the following series:

Reserved Properties

There is a large set of reserved properties provided by MSBuild that you can leverage in your build script. You access the reserved properties in the same way as your own properties using $(Reserved Property) expression. As an example let us add a new target to our build script:

<Target Name="Location">
    <Message Text="This construction is build at: $(MSBuildProjectDirectory)" />
</Target>

Then we can run the script by invoking the new target:

>msbuild construction.msbuild /target:Location

MSBuild Reserved Properties
As you can see the reserved property prints out the location of script file. A list with all reserved properties is available here.

Items

Items are similar to properties in that they can be used to store data in memory. But where a property is a scalar value, we can think of items as an array, and they typically represent files.

Let us start with a simple example and create some items representing different building materials.

<ItemGroup>
    <Materials Include="Concrete" />
    <Materials Include="Wood" />
    <Materials Include="Doors" />
    <Materials Include="Windows" />
</ItemGroup>

Okey, we now have our list of building materials, let us create a target that prints the materials to the screen. The syntax is similar to accessing properties, but instead of $ the @ symbol is used. This will loop through our list and print it out to the screen.

<Target Name="ListMaterials">
    <Message Text="@(Materials)" />
</Target>

Ok let us see if our script works, you should know the drill by now :)

>msbuild construction.msbuild /target:Materials

MSBuild Simple Items
Ta da! Thats great we now know what materials we have, but would it not be great if we could see the amount of each material? You guessed right, we can! It is possible to atach metadata to items, so let us do that by changing our list of materials to the following:

<ItemGroup>
    <Materials Include="Concrete">
        <Amount>5 ton</Amount>
    </Materials>
    <Materials Include="Wood">
        <Amount>500 rm</Amount>
    </Materials>
    <Materials Include="Doors">
        <Amount>10</Amount>
    </Materials>
    <Materials Include="Windows">
        <Amount>20</Amount>
    </Materials>
</ItemGroup>

That was easy, let us see how we can access the metadata then by updating our ListMaterials target:

<Target Name="ListMaterials">
    <Message Text="@(Materials)" />
    <Message Text="@(Materials->'%(Amount)')" />
</Target>

This will go through all materials and print out the amount property. To verify, run the target as before:

>msbuild construction.msbuild /target:ListMaterials

MSBuild Items Metadata
All good so far, let us see if we can use this to list files instead. But first we have to create som files and the easiest way to do this, is by using the Command Line and type the following commands:

>mkdir blueprints
>cd blueprints
>echo.>blueprint-1.txt
>echo.>blueprint-2.txt
>echo.>blueprint-3.txt
>cd ..

Before we create our items let us store the path in a new Property for reusability:

<PropertyGroup>
   <FoundationIsReady>true</FoundationIsReady>
   <WallsStatus>Pending</WallsStatus
   <BlueprintsPath>
       $(MSBuildProjectDirectory)\blueprints
   </BlueprintsPath>
</PropertyGroup>

Then we can add a new ItemGroup with our files:

<ItemGroup>
    <Blueprints Include="$(BlueprintsPath)\blueprint-1.txt" />
    <Blueprints Include="$(BlueprintsPath)\blueprint-2.txt" />
    <Blueprints Include="$(BlueprintsPath)\blueprint-3.txt" /></ItemGroup>

And finally let us add a new target to list our files. Items depending of type can have metadata by default, which is the case for files. For more information, go here. We can for example access the metadata for when the file was last change by using the ModifiedTime.

<Target Name="ListBlueprints">
    <Message Text="@(Blueprints)" />
    <Message Text="@(Blueprints->'%(ModifiedTime)')" />
</Target>

Ok let us run our script with the new target:

>msbuild construction.msbuild /target:ListBlueprints

MSBuild Items Files
Nice! But if we have several files in the directory it would be hard to maintain the script. Think if we hade to update script every time we added or removed a file. Luckily we can use wildcards to load all files with a specific extension. Change the blueprints item group with the following code:

<ItemGroup>
   <Blueprints Include="$(BlueprintsPath)\*.txt" />
</ItemGroup>

And then run the script again to verify that the output is the same as before.

>msbuild construction.msbuild /target:ListBlueprints

Logging verbosity

It can be very useful to control the logging verbosity when running a build script. This can we do with the /verbosity:[level]command which is one of many commands that you can use, to read more about available commands go here. To test out how we can control the logging verbosity we need to add a new target:

<Target Name="Log">
    <Message Text="Low Importance Message" Importance="low">
    <Message Text="Normal Importance Message" Importance="normal">
    <Message Text="High Importance Message" Importance="high">
</Target>

There are five levels of logging verbosity, quiet, minimal, normal, detailed and diagnostic.

The quiet level turns logging of as you can see in the following example:

>msbuild construction.msbuild /target:Log /verbosity:quiet

MSBuild Verbosity Quiet
The minimal level only logs messages that are of high importance.

>msbuild constuction.msbuild /target:Log /verbosity:minimal

MSBuild Verbosity Minimal
The normal level is the same as leaving the verbosity command out. It will print both high and normal importance messages, and also some more information like execution time. You have already seen this, so let us skip it and go directly to the next level.

The detailed level logs messages of any importance and also logs additional information about which target is executing.

>msbuild construction.msbuild /target:Log /verbosity:detailed

MSBuild Verbosity Detailed
Finally the diagnostic level outputs all the detail of a build that we can possible get. It is a lot of information, even to much to fit an image, so it is better if you try it out by your self.

>msbuild construction.msbuild /target:Log /verbosity:diagnostic

Organization

So far we have managed our script in one single file, the construction.msbuild file. But there are conventions that specify how you should organize a MSBuild project, so let us modify our project to meet those.

First of all we have used the file extension .msbuild, but as you now the build script is just a xml file so the file extension doesn't really matter. But let us follow those conventions. All MSBuild script should have at least one target that is the entry point to execute the script. This target should have the file extensions .proj. We can also have files that are used to store targets or properties that we can use across multiple build scripts. These files have the extensions .targets and .props.

Finally we can use a file to store command line arguments. This can be very useful because when using MSBuild you will end up needing to use lots of command line arguments. To follow the convention this file should have the extension .rsp.

Ok let us apply this to our project. We can start by changing the file extension of our build script to .proj with the following command:

>ren construction.msbuild construction.proj

Then let us create a .targets and a .props file:

>echo.>common.targets
>echo.>common.props

Let us start with our .targets file, open it up with your editor and type the following or copy it from the construction.projfile.

<?xml version="1.0" encoding="utf-8"?>  
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <FoundationIsReady>true</FoundationIsReady>
        <WallsStatus>Pending</WallsStatus>
        <BlueprintsPath>
            $(MSBuildProjectDirectory)\blueprints
        </BlueprintsPath>
    </PropertyGroup>
    
    <ItemGroup>
        <Materials Include="Concrete">
            <Amount>5 ton</Amount>
        </Materials>
        <Materials Include="Wood">
            <Amount>500 rm</Amount>
        </Materials>
        <Materials Include="Doors">
            <Amount>10</Amount>
        </Materials>
        <Materials Include="Windows">
            <Amount>20</Amount>
        </Materials>
    </ItemGroup>
    
    <ItemGroup>
        <Blueprints Include="$(BlueprintsPath)\*.txt" />
    </ItemGroup>
</Project>

And then let us put some of our targets from the construction.proj into the common.targetsfile.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="Foundation" BeforeTargets="Walls">
        <Message Text="Foundation for construction done"  />
    </Target>
    
    <Target Name="Roof" AfterTargets="Walls" Condition="$(WallsStatus) == 'Ready'">
        <Message Text="Roof for construction done" />
    </Target>
</Project>

To use the targets and properties files we have to import them in the construction.proj file.

<?xml version="1.0" encoding="utf-8"?>  
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Import Project="common.props" />
    <Import Project="common.targets" />
    
    <Target Name="Log">
        <Message Text="Low importance message" Importance="low" />
        <Message Text="Normal importance message" Importance="normal" />
        <Message Text="High importance message" Importance="high" />
    </Target>
    
    <Target Name="ListMaterials">
        <Message Text="@(Materials)" />
        <Message Text="@(Materials->'%(Amount)')" />
    </Target>
    
    <Target Name="ListBluePrints">
        <Message Text="@(Blueprints)" />
        <Message Text="@(Blueprints->'%(ModifiedTime)')" />
    </Target>
  
    <Target Name="Location">
        <Message Text="This construction is build at: $(MSBuildProjectDirectory)" />
    </Target>

    <Target Name="Walls" Condition="$(FoundationIsReady)">
        <Message Text="Walls for construction done" />
    </Target>
</Project> 

Let us verify that our modification work by invoking the Walls target.

>msbuild construction.proj /target:Walls

MSBuild Organization
Finally let us create a response file to store the command line arguments that we have used. Type the following command to create the file:

>echo.>args.rsp

Then open the file in your editor and type:

/target:Walls /verbosity:detailed

To use the response file you execute your script as follows:

>msbuild construction.proj @args.rsp

MSBuild Response File

We are gettings closer to see how Visual Studio use MSBuild to build projects, but before we look into that we will look at how we can create our own custom tasks. But I think we learnt enough today so we will check that out the next time. See you!

MSBuild Part 3: Get Organized
Share this