Tasks and Built-In Tasks
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The C# Reference bibliography lists the reference material consulted while preparing these pages. |
Targets order the work; tasks do it. A task is a .NET class implementing Microsoft.Build.Framework.ITask;
in a target it appears as an element named after the task, with its input parameters as attributes and <Output>
children that capture its output parameters into properties or items. MSBuild ships more than sixty tasks — the
compiler (Csc), file operations, Exec for running any command, MSBuild for building other projects — and the SDK
and packages add more. This page shows the ones used most, with MSBuild 18.9 (.NET SDK 10.0.402); writing your own
is covered on Inline Tasks and
Writing Custom Tasks.
Calling a task
<Copy SourceFiles="@(Journal)" DestinationFolder="$(OutDir)journals" SkipUnchangedFiles="true">
<Output TaskParameter="CopiedFiles" ItemName="Copied" />
</Copy>
-
Parameters map to the public settable properties of the task class. A parameter typed
stringtakes a property or text;ITaskItem[]takes an item list (with metadata);booltakestrue/false. A parameter marked[Required]must be set. -
Outputcopies an output property of the task into an item list (ItemName) or a property (PropertyName). -
Conditionskips the task;ContinueOnErrordecides what a failure does:false/ErrorAndStop(default),true/WarnAndContinue, orErrorAndContinue. -
Before a task can be used it must be declared with
UsingTask; the built-in tasks are declared inMicrosoft.Common.tasks, which MSBuild loads for every project. Tasks in.OverrideTasksfiles next to MSBuild take precedence over all others.
A parameter typed as an item list cannot be built by concatenating text with an item list — MSBuild reports it at the point of the call:
<WriteLinesToFile File="$(OutDir)summary.txt" Lines="lines: @(JournalLine->Count())" Overwrite="true" />
~/tasks/tasks.proj(19,51): error MSB4012: The expression "lines: @(JournalLine->Count())" cannot be used in this context. Item lists cannot be concatenated with other strings where an item list is expected. Use a semicolon to separate multiple item lists.
Compute the value into a property first (<JournalLineCount>@(JournalLine->Count())</JournalLineCount> inside the
target) and pass lines: $(JournalLineCount).
A tour of the built-in tasks
<Project DefaultTargets="Tour">
<PropertyGroup>
<OutDir>$(MSBuildProjectDirectory)/out/</OutDir>
</PropertyGroup>
<ItemGroup>
<Journal Include="journals/*.ledger.csv" />
</ItemGroup>
<Target Name="Tour">
<MakeDir Directories="$(OutDir)" />
<Copy SourceFiles="@(Journal)" DestinationFolder="$(OutDir)journals" SkipUnchangedFiles="true">
<Output TaskParameter="CopiedFiles" ItemName="Copied" />
</Copy>
<Message Importance="high" Text="Copied: @(Copied)" />
<ReadLinesFromFile File="@(Journal)">
<Output TaskParameter="Lines" ItemName="JournalLine" />
</ReadLinesFromFile>
<PropertyGroup>
<JournalLineCount>@(JournalLine->Count())</JournalLineCount>
</PropertyGroup>
<WriteLinesToFile File="$(OutDir)summary.txt" Lines="lines: $(JournalLineCount)" Overwrite="true" />
<XmlPeek XmlInputPath="ledger.config.xml" Query="/config/ledger/@currency">
<Output TaskParameter="Result" PropertyName="Currency" />
</XmlPeek>
<Copy SourceFiles="ledger.config.xml" DestinationFolder="$(OutDir)" />
<XmlPoke XmlInputPath="$(OutDir)ledger.config.xml" Query="/config/ledger/@currency" Value="USD" Condition="Exists('$(OutDir)ledger.config.xml')" />
<Message Importance="high" Text="Currency from XmlPeek: $(Currency)" />
<Exec Command="wc -l < "$(OutDir)journals/jan.ledger.csv"" ConsoleToMSBuild="true" StandardOutputImportance="low">
<Output TaskParameter="ConsoleOutput" PropertyName="LineCount" />
<Output TaskParameter="ExitCode" PropertyName="ExecExit" />
</Exec>
<Message Importance="high" Text="Exec: wc printed '$(LineCount)', exit code $(ExecExit)" />
<MSBuild Projects="child/child.proj" Targets="Describe" Properties="Region=EU">
<Output TaskParameter="TargetOutputs" ItemName="ChildOutput" />
</MSBuild>
<Message Importance="high" Text="MSBuild task returned: @(ChildOutput)" />
<CallTarget Targets="Archive" />
<Warning Text="Ledger warning example" Code="LL1001" />
<Error Text="never shown" Condition="'$(Currency)' == ''" />
</Target>
<Target Name="Archive">
<ZipDirectory SourceDirectory="$(OutDir)journals" DestinationFile="$(OutDir)journals.zip" Overwrite="true" />
<Delete Files="$(OutDir)summary.txt">
<Output TaskParameter="DeletedFiles" ItemName="Deleted" />
</Delete>
<Message Importance="high" Text="Archived to journals.zip, deleted @(Deleted->'%(Filename)%(Extension)')" />
</Target>
</Project>
dotnet msbuild tasks.proj -nologo -v:m
cat out/ledger.config.xml
Copied: ~/tasks/out/journals/jan.ledger.csv
Currency from XmlPeek: EUR
Exec: wc printed '2', exit code 0
MSBuild task returned: child built for EU
Zipping directory "~/tasks/out/journals" to "~/tasks/out/journals.zip".
Archived to journals.zip, deleted summary.txt
~/tasks/tasks.proj(43,5): warning LL1001: Ledger warning example
<config>
<ledger currency="USD" />
</config>
child/child.proj has a single Describe target that returns an item built from $(Region).
| Task | Notes |
|---|---|
|
Log text; |
|
Runs a command through the shell ( |
|
|
|
File system; |
|
Lines as items; |
|
Read or write with an XPath query ( |
|
Archives and downloads ( |
|
Builds other projects with |
|
Runs targets of the same project. Properties and items they create are not visible to the calling target until it
ends — prefer |
When Exec fails
Exec fails the build when the command’s exit code is not zero, and also when the command prints something that looks
like an MSBuild error (error XYZ: ...) unless IgnoreStandardErrorWarningFormat is set:
<Project>
<Target Name="ExecFails">
<Exec Command="grep -q 9999-Unknown journals/jan.ledger.csv" />
</Target>
<Target Name="ExecTolerated">
<Exec Command="grep -q 9999-Unknown journals/jan.ledger.csv" IgnoreExitCode="true">
<Output TaskParameter="ExitCode" PropertyName="GrepExit" />
</Exec>
<Message Importance="high" Text="grep exit code $(GrepExit) (ignored)" />
<Exec Command="grep -q 9999-Unknown journals/jan.ledger.csv" ContinueOnError="WarnAndContinue" />
<Message Importance="high" Text="still running after a failed Exec" />
</Target>
</Project>
== ExecFails
~/tasks/errors.proj(3,5): error MSB3073: The command "grep -q 9999-Unknown journals/jan.ledger.csv" exited with code 1.
== ExecTolerated
grep exit code 1 (ignored)
~/tasks/errors.proj(10,5): warning MSB3073: The command "grep -q 9999-Unknown journals/jan.ledger.csv" exited with code 1.
still running after a failed Exec
The classic Exec bug is an unquoted path. The command is a single string passed to the shell, so a property that
contains a space splits into two arguments:
<PropertyGroup>
<DataDir>$(MSBuildProjectDirectory)/ledger data</DataDir>
</PropertyGroup>
<Target Name="Unquoted">
<Exec Command="wc -l $(DataDir)/jan.ledger.csv" />
</Target>
<Target Name="Quoted">
<Exec Command="wc -l "$(DataDir)/jan.ledger.csv"" />
</Target>
== Unquoted
wc: ~/tasks/ledger: No such file or directory
wc: data/jan.ledger.csv: No such file or directory
0 total
~/tasks/quote.proj(6,5): error MSB3073: The command "wc -l ~/tasks/ledger data/jan.ledger.csv" exited with code 1.
== Quoted
2 ~/tasks/ledger data/jan.ledger.csv
Write " around every path. When the command is longer than a line or platform-specific, a dedicated task (or a
script file) is easier to get right than a quoted Exec.
Source: Exec task, MSBuild error MSB3073.
When Copy fails
Copy can retry before giving up. The Copy task page says the task’s default is zero retries and that callers pass
$(CopyRetryCount), but with MSBuild 18.9 a Copy with no Retries attribute retried 10 times, 1000 ms apart, and
$(CopyRetryCount) was empty. Each failed attempt logs MSB3026, the last one MSB3027, and the
failure itself MSB3021. Copying to a folder that cannot exist (/proc is a kernel
file system) with two retries:
<Copy SourceFiles="journals/jan.ledger.csv" DestinationFolder="/proc/ledgerline" Retries="2" RetryDelayMilliseconds="100" />
~/tasks/errors.proj(14,5): warning MSB3026: Could not copy "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Beginning retry 1 in 100ms. Could not find file '/proc/ledgerline'.
~/tasks/errors.proj(14,5): warning MSB3026: Could not copy "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Beginning retry 2 in 100ms. Could not find file '/proc/ledgerline'.
~/tasks/errors.proj(14,5): error MSB3027: Could not copy "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Exceeded retry count of 2. Failed.
~/tasks/errors.proj(14,5): error MSB3021: Unable to copy file "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Could not find file '/proc/ledgerline'.
On Windows the usual cause is a locked file — the app or a test host is still running from bin/. The fix is to stop
the process; the retries exist because antivirus scanners and
indexers often hold a file for a moment. Change wave 18.7 made Copy retry on access-denied errors on non-Windows
platforms too.
Source: Copy task, MSB3026, MSB3027, MSB3021, Change waves (18.7).
Common mistakes
-
Unquoted paths in
Exec— see above. -
Execfor things a task does portably (copy, delete, zip, XML edits) — the shell differs between Windows and Unix; the tasks do not. -
Text concatenated with an item list in an
ITaskItem[]parameter —MSB4012. -
CallTargetto compute values for the calling target — they are not visible until the caller finishes. -
ContinueOnError="true"everywhere — turns real failures into warnings; scope it to the one task that may fail. -
WriteLinesToFilewithoutWriteOnlyWhenDifferentin an incremental chain — a fresh timestamp invalidates every target that reads the file.