In this second installment in the series, I’ll showcase the core concepts in MSBuild that will get you up and running quickly, with concrete examples to try out the various constructs.
If you have never tweaked your .csproj
s in any significant way (i.e. beyond adding
a Condition
here and there or tweaking a properly value), a quick overview of the
core concepts in MSBuild would be helpful before we move on to more advanced topics.
A build typically uses properties (i.e. Configuration
with a value of Release
)
and items (i.e. all .cs
files in a project) to produce some output (i.e. a .dll
,
.nupkg
, etc.) by processing then using tasks (i.e. Copy
a file, or Csc
to invoke the
C# compiler), which are grouped in targets (i.e. Build
, Compile
, Publish
and so
on).
A farily useful analogy with a programming language might be:
- properties > scalar (string) values
- items > arrays
- target > method declaration
- task > method invocation
Make sure you create an empty (say) test.proj
file in a new empty directory,
keep it open in an editor (i.e. VS Code or VS 2017+) and also keep a developer
command prompt opened in the same directory as we go along. That’s key to
learning this thing ;).
I suggest you use the following MSBuild switches on every msbuild
run from
now on:
/nologo
: avoids rendering the MBuild version and copyright information, unnecessary in order to learn to learn MSBuild ;)/v:m
: short form of/verbosity:minimal
so that you only see output we render explicitly and nothing more. Useful when learning to cut down the noise
Optionally, for the more advanced scenarios and to understand more of the inner workings and the MSBuild evaluation:
/bl
: automatically generates anmsbuild.binlog
file for advanced inspection of the build process. Can be opened with the MSBuild Structured Log Viewer on Windows
Pro Tip: except for the core MSBuild XML elements, it’s important to remember that pretty much everything is case-insensitive in MSBuild, even target, property, item and task names and even task parameters!
Targets
Without a Target
to execute, a project cannot do anything. There is nothing to invoke. All a target needs at a minimum is a name, like:
<Project>
<Target Name="Test" />
</Project>
If you run msbuild /nologo /v:m
you will esentially get an empty output in the console.
Even if such a target seems really pointless right now, there are use cases where it’s
actually useful, which we’ll learn in a future post.
To compare the output, running just msbuild
would get you something like:
Microsoft (R) Build Engine version 15.3.409.57025 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.
Build started 9/22/2017 12:42:49 AM.
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.32
and msbuild /nologo
:
Build started 9/22/2017 12:43:22 AM.
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.32
From now on I’ll omit /nologo
and /v:m
but remember to add them always to get the
clean output.
NOTE: unlike Tasks, Targets cannot receive arguments, so they are more like parameterless void methods or entry points (i.e.
void Main()
).
If your project has more than one target, you can specify which one to run with
msbuild /t:TARGET
, such as msbuild /t:test
(note that the target name is case-insensitve
too). You can also specify that more than one target should run in sequence, like
msbuild /t:build;test
Our empty target would essentially be the equivalent of an empty-bodied method. It needs a body now to do something, which is typically made of tasks.
Tasks
As mentioned, you can think of a task as a (optionally parameterized) method invocation. There are many built-in tasks for common operations (like copying, reading, writing and deleting files, executing external tools and so on), but you can also author and consume custom tasks.
One very useful task while learning is Message
,
which receives just two arguments:
Text
and Importance
and renders that to the output:
<Project>
<Target Name="Test">
<Message Text="Hello World!" Importance="high" />
</Target>
</Project>
If you run msbuild
you will now get the expected message in the output window:
C:\source\test>msbuild test.proj /nologo /v:m
Hello World!
C:\source\test>
Note that since almost everything is case-insensitive in MSBuild, you could also have written that as:
<Project>
<Target Name="Test">
<message text="Hello World!" importance="High" />
</Target>
</Project>
But intellisense for the built-in tasks will guide you towards the PascalCase
style typically.
Now let’s say you want to allow the build to configure the message to render in that task. That’s what you’d use a Property for.
Properties
As mentioned, a property contains a scalar (string) value, and is declared within a
<PropertyGroup>
enclosing element, such as:
<Project>
<PropertyGroup>
<Message>Hello World!</Message>
</PropertyGroup>
</Project>
Properties are referenced using the $
sign and enclosed in parenthesis, like $(Configuration)
.
Let’s replace the Text
parameter in the task with the property:
<Project>
<PropertyGroup>
<Message>Hello World!</Message>
</PropertyGroup>
<Target Name="Test">
<Message Text="$(Message)" Importance="high" />
</Target>
</Project>
Running msbuild
will yield the exact same output as before, since we haven’t changed the contents
of the message.
Note that there is no ambiguity nor conflict between a Task named Message
and a Property named
the same, since the XML element is used in different contexts: the former can only be used as a
direct child of <Target>
and the latter as a child of <PropertyGroup>
. Referencing the property
always happens either inside an attribute or inside the content of another property, and is always
wrapped with $()
.
You can also mix constant strings with property references, such as:
<Project>
<PropertyGroup>
<Name>kzu</Name>
</PropertyGroup>
<Target Name="Test">
<Message Text="Hello $(Name)!" Importance="high" />
</Target>
</Project>
output:
C:\source\test>msbuild test.proj /nologo /v:m
Hello kzu!
Finally, you can also reference properties when declaring other properties:
<Project>
<PropertyGroup>
<Name>kzu</Name>
<message>Hello $(name)!</message>
</PropertyGroup>
<Target Name="Test">
<Message Text="$(Message)" Importance="high" />
</Target>
</Project>
(which would render the same message as the previous example)
Note that the property names are case-insensitive, and although you could use such a confusing mixed casing, I don’t recommend doing so for obvious readability reasons ;).
Properties are fully mutable at any point during execution, and they are evaluated in document order. If you reference a property before it is declared, or a non-existent property altogether, you just get an empty string. There is simply no way to distinguish between an undeclared property and one that has an empty value.
You can also specify a value for a property via the command line, like msbuild /p:name=Daniel
:
Hello Daniel!
It might be a bit confusing to see that message since we are clearly setting the value of the property
to something else in the project. The reason for this is that by default, any MSBuild property can be overriden with the command line /p
switch, even if it’s assigned unconditionally in the project.
In order to avoid this default behavior and allow the project-determined value to prevail, you can
add the following attribute to the Project
:
<Project TreatAsLocalProperty="Name">
...
This signals to MSBuild that the project can modify the value of the property locally. If you run
again msbuild /p:name=Daniel
, you will see that the message is Hello kzu!
this time around.
In addition, environment variables are automatically exposed as properties, such as USERNAME
.
Basically everything you can echo %VARNAME%
in the console, you can reference in MSBuild with
$(VARNAME)
.
Pro Tip: somewhat confusingly, environment variables are always overridable by the MSBuild project, without needing the
TreatAsLocalProperty
project attribute.
In the above example, we could replace the hardcoded kzu
with the corresponding envvar instead:
<Project>
<PropertyGroup>
<Name>$(USERNAME)</Name>
<Message>Hello $(Name)!</Message>
</PropertyGroup>
<Target Name="Test">
<Message Text="$(Message)" Importance="High" />
</Target>
</Project>
Items
Say we want to allow more than one name above. We’d use an item group instead:
<Project>
<ItemGroup>
<Name Include="kzu" />
<Name Include="daniel" />
</ItemGroup>
</Project>
Items are added to the “array” Name
(the element name within the item group becomes the
name of the array, so to speak) by “including” them via the Include
attribute.
The simplest way to reference the items is with @(ITEM)
:
<Target Name="Test">
<Message Text="Hello @(Name)" Importance="High" />
</Target>
would render:
Hello kzu;daniel
The Text
attribute/parameter for the Message
task
is a simple string, and therefore an
automatic conversion from the array @()
to a string scalar is performed by concatenating
the items with the default separator ;
. In this case, it would look better if we changed
the separator to a ,
instead, which can be done as follows:
<Target Name="Test">
<Message Text="Hello @(Name, ', ')." Importance="High" />
</Target>
Which would render on build like:
Hello kzu, daniel.
If we wanted to render a full message for each name, we’d need to foreach
on the items.
This can be done by using the %()
construct instead. Update the target as follows:
<Target Name="Test">
<Message Text="Hello %(Name.Identity)." Importance="High" />
</Target>
Which would render on build like:
Hello kzu.
Hello daniel.
This is called batching and is a key concept we will explore further in future posts because it’s key to several advanced scenarios including incremental builds.
Note that we can’t just do %(Name)
. The Identity
is a built-in item metadata which is
the value of the Include
element in the <Name>
item declarations. Items can also have
custom metadata, such as:
<Project>
<ItemGroup>
<Name Include="kzu" FullName="Daniel Cazzulino" />
<Name Include="daniel" FullName="ditto ;)" />
</ItemGroup>
</Project>
We could reference the new custom metadata just like the built-in metadata:
<Target Name="Test">
<Message Text="Hello %(Name.FullName)." Importance="High" />
</Target>
Conditions
Conditionally changing the outcome of the build is also a core characteristic in most cases,
such as building differently for Debug
vs Release
builds. MSBuild has a consistent and
general mechanism for conditionally evaluating all the core elements discussed so far, by
simply appending a Condition
attribute on them. A condition attribute must evaluate to
a string that can be parsed as a boolean (in a case insensitive manner, as everything else,
such as ‘true’ or ‘False’);
Going back to our example of the overridable Name
property for the greeting message,
we might want to make the property overridable either via a command line /p:Name=
argument
or via an environment variable. As mentioned, the first case is always covered even if we
assign a “default” hardcoded value within the project, but to account for the environment
variable override, we must check for an empty value, before assigning the “default”, since
otherwise we’d overwrite the envvar-provided value:
<PropertyGroup>
<Name Condition="'$(Name)' == ''">kzu</Name>
<Message>Hello $(Name)!</Message>
</PropertyGroup>
That property can now be safely overriden by both environment variables and command line arguments.
The condition could be placed in the entire PropertyGroup
too, for the case where the
entire Message
is overriden:
<PropertyGroup Condition="'$(Message)' == ''">
<Name Condition="'$(Name)' == ''">kzu</Name>
<Message>Hello $(Name)!</Message>
</PropertyGroup>
Conditions on targets skip the entire target’s execution:
<Project>
<Target Name="Test" Condition="'$(RunTest)' == 'true'">
<Message Text="$(Message)" Importance="High" />
</Target>
</Project>
Individual tasks can also be conditioned exactly the same way:
<Project>
<Target Name="Test">
<Message Condition="'$(Greet)' == 'true'" Text="$(Message)" Importance="High" />
</Target>
</Project>
That’s basically it for the very basic concepts that you can use to start creating build scripts, IMO. You can read more at the official MSBuild Concepts documentation.