How to initialize items in an ItemGroup with default values? - msbuild

A property can be initialized to a default value in a PropertyGroup like this:
<PropertyGroup>
<MyProperty Condition="$(MyProperty) == ''">MyDefaultValue</MyProperty>
</PropertyGroup>
Is it possible to achieve the same with Items?:
<ItempGroup>
<MyItems Condition="MyItems is no defined" Include="MyDefaultFile.ext;"/>
</ItemGroup>
Currently I'm doing:
<ItemGroup>
<MyItems Condition="!Exists(#(MyItems))" Include="MyDefaultFile.ext;"/>
</ItemGroup>
But I don't think this is good idea (i.e The files in MyItems may not exist, while MyItems may be defined)

Set the condition on the ItemGroup level:
<ItemGroup Condition="'#(MyItems)' == ''">
<MyItems Include="MyDefaultFile.ext" />
</ItemGroup>
You can also use ItemDefinitionGroup to initialize default metadata values.

Related

msbuild empty property value is threated as null value

I have a task configuration property RelativePathOverride defined like this:
<PropertyGroup>
<RelativePathOverride>..\..\</RelativePathOverride>
</PropertyGroup>
and then used as
<Target Name="CustomTaskTarget" AfterTargets="PostBuildEvent">
<SimpleTask RelativePathOverride="$(RelativePathOverride)"/>
</Target>
The task code is following:
public class SimpleTask : Task
{
public string RelativePathOverride { get; set; }
public override bool Execute()
{
Log.LogMessage(MessageImportance.High, $"RelativePathOverride: {RelativePathOverride ?? "NULL"}");
if (RelativePathOverride == null)
{
// default value
RelativePathOverride = "..\\";
}
}
}
This works fine. However, the problem is that when I provide the empty value for the RelativePathOverride property then it's defaulted to NULL!
<PropertyGroup>
<RelativePathOverride></RelativePathOverride>
</PropertyGroup>
In my logic - I want the empty value to be an empty value! This is very important because we are talking about a relative path. NULL value means that there is NO override provided, so the default will be hardcoded to ..\. But since empty property value is also threated as null then this corrupts my logic..
Is there is native approach to allow passing empty values into property?
p.s. on a side note (might be related) - 1 space is also threated as null..
In most instances, comparisons in msbuilds usually interpret "empty" and null as the same thing. I suggest using .\ or . in your case.

How can I pass a key-value set of parameters to a task in MsBuild?

I have a custom task that requires a set of key-values in order to work. How can I get a custom MSBuild configurable with a string-to-string dictionary sort of configuration?
There is no built-in dictionaries in MSBuild, but you can make up your own that would behave almost like a dictionary. There are several options, but the semantically the closest one would be to use use an item group with metadata for key and value.
Your MSBuild file might look like this:
<ItemGroup>
<MyDictionary Include="Value1">
<MyKey>key1</MyKey>
</MyDictionary>
<MyDictionary Include="Value2">
<MyKey>key2</MyKey>
</MyDictionary>
...
<MyDictionary Include="ValueN">
<MyKey>keyN</MyKey>
</MyDictionary>
</ItemGroup>
<Target Name="MyTarget">
<MyTask MyInput="#(MyDictionary)" ... />
</Target>
Your custom task will simply take an input of ITaskItem[] array, and you can iterate through it to convert it to real Dictionary if you need to:
class MyTask: ITask
{
public ITaskItem[] MyInput { get; set; }
public override bool Execute()
{
...
var dic = new Dictionary<string, string>();
foreach (var input in MyInput)
{
dic.Add(input.GetMetadata("MyKey"), input.ItemSpec);
}
...
}
}
Note that ItemGroup does not guarantee one to one mapping between keys and values, so you might end up with multiple values for the same key.
If you have a set number of key value pairs where the keys don't change but the values do, you could use good old fashion environment variables to accomplish this.
#ECHO OFF
SET K1=Value1
SET K2=Value2
MSBUILD.EXE mytest.proj
Then in your mytest.proj file you have something like :
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="ATest" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="ATest">
<Message Text="-- $(K1) --"/>
<Message Text="-- $(K2) --"/>
</Target>

Custom msbuild task output not recognized

I have a custom msbuild task with an output parameter defined below
public class DeployJavaScript : Task
{
[Required]
public ITaskItem[] SourceFiles { get; set; }
[Output]
public string Result { get; set; }
#region Overrides of Task
public override bool Execute()
{
foreach (var sourceFile in SourceFiles)
{
}
Result = String.Format("Sucessfully Deployed Javascript Files");
return true;
}
#endregion Overrides of Task
}
In my build script(csproj file) I extend msbuild by injecting my custom task in the AfterBuild target as defined below
<Target Name="AfterBuild">
<Message Text="AfterBuild Begin" Importance="high"/>
<PropertyGroup>
<JavaScriptFolderPath Condition=" '$(JavaScriptFolderPath)' == '' " >$(MSBuildProjectDirectory)\</JavaScriptFolderPath>
<JavaScriptFilePath></JavaScriptFilePath>
</PropertyGroup>
<ItemGroup>
<JavaScriptFolderFiles Include="$(JavaScriptFolderPath)\**\*.js"/>
</ItemGroup>
<ItemGroup>
<JavaScriptFiles Include="$(JavaScriptFilePath)"/>
</ItemGroup>
<DeployJavaScript SourceFiles="#(JavaScriptFolderFiles->'%(FullPath)')">
<Output TaskParameter="Result" PropertyName="ResultofJavaScriptDeployment"/>
</DeployJavaScript>
<Message Text="$(ResultofJavaScriptDeployment)" Importance="high"/>
<Message Text="AfterBuild Complete" Importance="high"/>
However, msbuild complains "Unknown output parameter Result,'DeployJavaScript' should have no output parameters"
Why I cannot return an output parameter in this scenario?
P.S
I know I can use Log.LogMessage(MessageImportance.high,"sucess",high) to log the result in the proj file which would serve my purpose. Just want to know why I cannot use an output parameter.
You have to change the type of the Result property in your code. Use ITaskItem instead of string. For me it helped to solve the same problem.
Naturally, your code will have to create an instance of TaskItem class after that:
Result = new TaskItem(String.Format("Sucessfully Deployed Javascript Files"));

Custom Task getting called more than once

I am learning how to use MSBuild recently so I decided to tackle writing my own custom MSBuild task. What I found is that MSBuild is calling my task just fine... but it calls it over and over and over again. It repeats the call to it many times, even though the msbuild project calls it only once.
Here is my project XML:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
<PropertyGroup>
<BuildDir>build directory specified here</BuildDir>
.. various other stuff here too
</PropertyGroup>
<Import Project="file1.xml" />
<Import Project="file2.xml" />
<UsingTask TaskName="CopyToBuild.Copy_To_Build"
AssemblyFile="CopyToBuild.dll" />
<Target Name="MyNewCopyTask">
<Copy_To_Build SourceFiles="#(copy_to_build)"
DestinationFolder="%(Destination)"
SkipUnchangedFiles="true"
BuildDirectory="$(BuildDir)" />
</Target>
</Project>
So as you can see I call my Copy_To_Build task only once in the project. I import an xml file that contains items that is passed in to the SourceFiles attribute of my Copy_To_Build task. Everything works great. Except for one thing: the problem is that my Execute method on my custom task gets called more than once.
public class Copy_To_Build : Microsoft.Build.Utilities.Task
{
[Required]
public ITaskItem[] SourceFiles { get; set; }
[Required]
public ITaskItem[] DestinationFolder { get; set; }
public String BuildDirectory { get; set; }
public bool Clean { get; set; }
public bool SkipUnchangedFiles { get; set; }
public override bool Execute()
{
Console.WriteLine("Build Directory: {0}", BuildDirectory);
...
}
}
I know it's getting called more than once because I put a print statement in there which reveals that the function is getting more than once. I expected it to get called only once.
Is it getting called more than once because I have some sort of threading option set? I put in a statement to print the current thread:
Console.WriteLine("Current Thread: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
But that revealed that everything was on the same thread.
Last but not least, here is the command line script I am using to call everything:
#echo off
call "%VS100COMNTOOLS%..\..\VC\vcvarsall.bat" x64
rem set some build properties
set MISC=/nologo /verbosity:Normal
set LOGGING=/fileLogger /fileloggerparameters:LogFile=msbuild_foo.log;Encoding=UTF-8;Verbosity=Normal
set PROPERTY=/property:Platform=x64;Configuration=DebugUnicode;BuildDir=E:\foo
set TARGET=/target:MyNewCopyTask
msbuild %MISC% %LOGGING% %PROPERTY% %TARGET% foo.xml
pause
#echo on
So in summary: Why is my task getting called more than once?
Thanks
<Copy_To_Build SourceFiles="#(copy_to_build)"
DestinationFolder="%(Destination)"
SkipUnchangedFiles="true"
BuildDirectory="$(BuildDir)" />
It will be called once per unique value of the "Destination" metadata. This is called 'batching'. You could either do it like the way it is working right now, or make DestinationFolder property optional, and in case it is not specified, then the task can look for the "Destination" metadata in the items "SourceFiles" and copy the item to that folder.
But the usual way to do it as you have right now, just make DestinationFolder a string.

How do you format text in MSBuild?

Specifically, I am looking to zero pad a number to create a string based label. i.e. build 7 into build 007. You can easily add strings together, but in all my searches on formatting, padding, strings, etc... I have not been able to find any references.
Example of what I am working with.
<PropertyGroup>
<FileParserVersion>File Parser $(Major).$(Minor).$(Build) Build $(Revision)</FileParserVersion>
<VersionComment>Automated build: $(FileParserVersion)</VersionComment>
</PropertyGroup>
This is generated: FILEPARSER_1_0_3_BUILD_7
What is preferred: FILEPARSER_1_0_3_BUILD_007
In 4.0+ you can do it in one line with Property Functions (and on MSDN)
$([System.String]::Format('FILEPARSER_$(Major)_$(Minor)_$(Build)_BUILD_{0:000}', $([MSBuild]::Add($(Revision), 0))))
Unfortunately the bogus "Add" is necessary to trick MSBuild to coerce $(Revision) to a number before it coerces it into the object expected by String.Format. If I don't do that it uses a string, and the padding doesn't work. The coercion inside MSBuild could be a bit smarter here.
Consider the following ITask:
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace My.MSBuild.Tasks
{
public class FormatRevision : Task
{
#region Public Properties
[Required]
public int Revision { get; set; }
[Required]
public string MajorVersion { get; set; }
[Output]
public string OutputVersion { get; private set; }
#endregion
#region ITask Methods
public override bool Execute()
{
OutputVersion = string.Format("{0}.{1}"
, MajorVersion
, Revision < 10 ?
"00" + Revision : Revision < 100 ?
"0" + Revision : Revision.ToString());
Log.LogMessage("Revision: {0} -> Output Version: {1}"
, Revision, OutputVersion);
return true;
}
#endregion
}
}
MSBuild target (formatvesion.proj):
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="FormatRevision">
<FormatRevision MajorVersion="$(MajorVersion)" Revision="$(Revision)">
<Output TaskParameter="OutputVersion" PropertyName="FormattedVersion"/>
</FormatRevision>
</Target>
<UsingTask TaskName="My.MSBuild.Tasks.FormatRevision" AssemblyFile="My.MSBuild.Tasks.dll" />
</Project>
Invoked by command:
msbuild formatvesion.proj /t:FormatRevision /p:MajorVersion=1.0;Revision=7
Alternatively, if you wish to use CreateProperty:
<PropertyGroup>
<FileParserVersion>File Parser $(Major).$(Minor).$(Build) Build $(Revision)</FileParserVersion>
<VersionComment>Automated build: $(FileParserVersion)</VersionComment>
</PropertyGroup>
<PropertyGroup>
<PaddedRevision Condition="$(Revision) < 1000">$(Revision)</PaddedRevision>
<PaddedRevision Condition="$(Revision) < 100">0$(Revision)</PaddedRevision>
<PaddedRevision Condition="$(Revision) < 10">00$(Revision)</PaddedRevision>
</PropertyGroup>
<Target Name="test">
<CreateProperty
Value="FILEPARSER_$(Major)_$(Minor)_$(Build)_BUILD_$(PaddedRevision)">
<Output TaskParameter="Value" PropertyName="MyFileVersion" />
</CreateProperty>
<Message Text="$(VersionComment) -> $(MyFileVersion)" />
</Target>