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.
Related
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>
I'm writing a custom build task and and would like to access the value of this property programmatically. It is possible? I've looked at all of the members of the Task class and didn't see anything that looked like it would allow me to get the value.
If the custom task can't run without a property, the best practice is to make that property a required input:
[Required]
public string OutDir { get; set; }
Set by:
<MyTask ... OutDir="$(OutDir)" />
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"));
I made a custom task in my TFS build to examine my project's GlobalAssemblyInfo.cs file in order to extract an attribute (AssemblyInformationalVersion to be exact) in order to use its value in naming a zip file that I make with the build.
<UsingTask TaskName="GetAssemblyInformationalVersion.GetAssemblyInformationalVersionTask"
AssemblyFile="$(MSBuildExtensionsPath)\GetAssemblyInformationalVersion.dll" />
The .cs file for my DLL has these two properties:
[Required]
public String InfoFile { get; set; }
public String InfoVersion { get; set; }
Here is my call to my task:
<GetAssemblyInformationalVersionTask InfoFile="$(Path to file)\GlobalAssemblyInfo.cs" />
My intention is to pass in the assembly info file through the property InfoFile so that I can find what I want (which my C# code does) and set it to the property InfoVersion for me to reference in TFS by running it as a task. In principle, I would use the property InfoVersion to use in naming my zip file. For example,
"Package.$(some form of reference to InfoVersion).zip"
However, I have not been able to find a way to actually accomplish this.
My question is: How can I invoke the get part of my property in my task? It seems like it should be easy, since I have not found anything written about this sort of thing online, but any help will be much appreciated.
Your custom task, GetAssemblyInformationVersionTask, will need to have a property on it of type ITaskItem that is decorated with the [Output] attribute.
public class GetAssemblyInformationVersionTask
{
[Output]
public ITaskItem Version { get; set; }
public override bool Execute()
{
// code to set Version
return true;
}
}
Then you will be able to use it like so:
<GetAssemblyInformationVersionTask InfoFile="$(Path to file)\GlobalAssemblyInfo.cs">
<Output TaskParameter="Version" ItemName="AssemblyVersion" />
</GetAssemblyInformationVersionTask>
AssemblyVersion will be the item variable that will contain the value of the Version property of your task.
If you've not seen it, MSDN Best Practices for Reliable Builds, Part 2 touches on the subject of output parameters. I'll see if I can't find better examples online.
Thomas Ardal has another good sample of [Output] in a custom task here.
HTH,
Z
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>