I can't figure out how to pass values into an MSBuild task like I would a method. Take the following project file...
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTargets="Main">
<PropertyGroup>
<Var1>Foo</Var1>
<Var2>Bar</Var2>
</PropertyGroup>
<Target Name="Main">
<Message Text="$(Var1)" Importance="high" />
<Message Text="$(Var2)" Importance="high" />
</Target>
</Project>
I want to refactor the Message task into a target and then pass over Var1 and Var2 to it to get the same output. This is a very simplified example but the concept is the same.
I think you want to do something like this:
<ItemGroup>
<Messages Include="Message1">
<Text>Hello from Message1</Text>
</Messages>
<Messages Include="Message2">
<Text>Hello from Message2</Text>
</Messages>
</ItemGroup>
<Target Name="TestMessage">
<Message Text="%(Messages.Text)"/>
</Target>
This produces the following output:
TestMessage:
Hello from Message1
Hello from Message2
This is meant to complement, not replace, #BryanJ’s answer.
There are two types of batching. One is Task batching which happens when you use %(ItemName.MetaData) syntax. You just specify this value into a task parameter as if %(ItemName.MetaData) would only ever expand to one particular value. MSBuild then automatically executes the task multiple times, avoiding the need for the task to explicitly support iterating over a list of items.
Another batching type is Target batching. Target batching happens when you use the Inputs and Outputs attributes of <Target/>. To batch over an arbitrary set of Items in such a way that the target gets executed exactly once per Item, you can specify Inputs="#(ItemName)" Outputs=%(Identity).bogus. What’s important is that %(Identity) is present. Batching will look at all the possible expansions of Inputs and Outputs and decide its batching based on these expansions. Thus, you must make sure that each item gets a unique expansion if you want the Target to run separately for each item. I give #BryanJ’s code with modifications to use Target batching of this style:
<?xml version="1.0"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTargets="all">
<ItemGroup>
<Messages Include="Message1">
<Text>Hello from Message1</Text>
<Group>1</Group>
</Messages>
<Messages Include="Message2">
<Text>Hello from Message2</Text>
<Group>1</Group>
</Messages>
<Messages Include="Message3">
<Text>Hello from Message3</Text>
<Group>2</Group>
</Messages>
</ItemGroup>
<Target Name="all" DependsOnTargets="TestMessage;TestMessageGrouping" />
<!--
Use the Inputs/Outputs attributes to specify Target
batching. The metadata value I am batching over is
Identity. Since Identity is unique per item, this means the
Target will get run in full once for every value in
Messages. We provide something bogus for Outputs. It is
important that our bogus values do not coincide with real
filenames. If MSBuild finds a file with the name of a value
in Outputs and another file, with an older timestamp,
matching the corresponding value in Inputs, it will skip
running this Target. (This is useful in many situations, but
not when we want to just print out messages!)
-->
<Target Name="TestMessage" Inputs="#(Messages)" Outputs="%(Identity).bogus">
<Message Text="I will print the Text metadata property of %(Messages.Identity)" />
<Message Text="%(Messages.Text)" />
</Target>
<!--
If you want to combine Task and Target batching, you can specify
a different metadata value than Identity to group the items
by. I use the Group metadata I specified in the ItemGroup.
-->
<Target Name="TestMessageGrouping" Inputs="#(Messages)" Outputs="%(Group).bogus">
<Message Text="I will print the Text metadata property of all messages from Group %(Messages.Group)" />
<!--
Now, within the Target batch, we use Task batching to print
all of the messages in our %(Messages.Group) at once.
-->
<Message Text="%(Messages.Text)" />
</Target>
</Project>
with output:
TestMessage:
I will print the Text metadata property of Message1
Hello from Message1
TestMessage:
I will print the Text metadata property of Message2
Hello from Message2
TestMessage:
I will print the Text metadata property of Message3
Hello from Message3
TestMessageGrouping:
I will print the Text metadata property of all messages from Group 1
Hello from Message1
Hello from Message2
TestMessageGrouping:
I will print the Text metadata property of all messages from Group 2
Hello from Message3
Related
For a C++ project, I want to autogenerate a defs.h file with project definitions, such as the date, git commit, ... to automate the versioning process of my application.
Therefore I am trying to create a MSBuild Target that will extract the latest git tag, git commit, and the current date and save it to a temporary gitinfo.txt file.
Another build target will depend on that file and generate a .h file.
In order to avoid unnecessary recompiles of my project, the .h file and for that reason the gitinfo.txt file shall only be rewritten, if any of the information has changes.
So my idea is the following:
Calculate git and date info
If available, read in the existing gitinfo.txt
Compare the calculated values to those in the txt file
If anything has changed, rewrite the gitinfo.txt
I've mastered steps 1. and 2., however I am not sure how to process the values after reading them.
<!-- The purpose of this target is to update gitinfo.txt if git information (commit...) has changed -->
<Target
Name="GetHeaderInfos"
BeforeTargets="ClCompile"
Outputs="$(IntDir)\gitinfo.txt"
>
<!-- Get information about the state of this repo-->
<GitDescribe>
<Output TaskParameter="Tag" PropertyName="NewGitTag" />
<Output TaskParameter="CommitHash" PropertyName="NewGitCommitHash" />
<Output TaskParameter="CommitCount" PropertyName="NewGitCommitCount" />
</GitDescribe>
<!-- Get the current date -->
<Time Format="dd.MM.yyyy">
<Output TaskParameter="FormattedTime" PropertyName="NewBuildDate" />
</Time>
<ReadLinesFromFile File="$(IntDir)\gitinfo.txt" Condition="Exists('$(IntDir)\gitinfo.txt')">
<Output TaskParameter="Lines" ItemName="Version" />
</ReadLinesFromFile>
<!-- Comparison here! HOW TO DO IT PROPERLY -->
<PropertyGroup>
<TagChanged> <!-- `$(NewGitTag)` == `$(Version)[0]` --> </TagChanged>
<!-- Other comparisons -->
</PropertyGroup>
</Target>
And this could be the content of gitinfo.txt
v4.1.4
04fe34ab
1
31.07.2016
I am not quite sure how to compare the values now. I need to compare $(NewGitTag) to the first value in the $(Version) version variable, and so on.
I haven't found an example, that actually accesses the variables after reading them from a file. The official documentation provides no help, nor have I found anything on stackoverflow or the likes.
I only know that the $(Version) variable holds a list, and I can batch process it. How can I compare its content to the defined variables $(NewGitTag), $(NewGitCommitHash), $(NewGitCommitCount) and $(NewBuildDate)?
Suppose we start with this data:
<ItemGroup>
<Version Include="v4.1.4;04fe34ab;1;31.07.2016"/>
</ItemGroup>
<PropertyGroup>
<GitTag>v4.1.4</GitTag>
<GitSHA>04fe34ab</GitSHA>
<Count>1</Count>
<Date>31.07.2016</Date>
</PropertyGroup>
Then here are at least 3 ways to achieve comparision (apart from the one mentioned in the comment) and there are probably other ways as well (I'll post them if I can come up with something else):
Just compare the items
I'm not sure why you want to compare everything seperately when this works just as well: compare the whole ItemGroup at once.
<Target Name="Compare1">
<PropertyGroup>
<VersionChanged>True</VersionChanged>
<VersionChanged Condition="'#(Version)' == '$(GitTag);$(GitSHA);$(Count);$(Date)'">False</VersionChanged>
</PropertyGroup>
<Message Text="VersionChanged = $(VersionChanged)" />
</Target>
Batch and check if there's one difference
Each item of Version is compared with e.g. GitTag via batching. The result will be False;False;False;False if there's a difference, else it will be True;False;False;False. Count the distinct elements and if it's 2 it means we got the latter so GitTag did not change. Note this obviousle only works if each of your source items can never have the same value as one of the other items.
<Target Name="Compare2">
<PropertyGroup>
<TagChanged>True</TagChanged>
<TagChanged Condition="'#(Version->Contains($(GitTag))->Distinct()->Count())' == '2'">False</TagChanged>
</PropertyGroup>
<Message Text="TagChanged = $(TagChanged)" />
</Target>
you can then compare the other items as well and combine the result.
Use an inline task to access items by index
This comes closest to what's in your question, but it does need a bit of inline code.
<UsingTask TaskName="IndexItemGroup" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<Items Required="true" ParameterType="Microsoft.Build.Framework.ITaskItem[]"/>
<Index Required="true" ParameterType="System.Int32"/>
<Item Output="true" ParameterType="Microsoft.Build.Framework.ITaskItem"/>
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs">
<![CDATA[Item = Items[ Index ];]]>
</Code>
</Task>
</UsingTask>
<Target Name="Compare3">
<IndexItemGroup Items="#(Version)" Index="1">
<Output PropertyName="OldGitSHA" TaskParameter="Item"/>
</IndexItemGroup>
<PropertyGroup>
<SHAChanged>True</SHAChanged>
<SHAChanged Condition="'$(GitSHA)' == '$(OldGitSHA)'">False</SHAChanged>
</PropertyGroup>
<Message Text="OldGitSHA = $(OldGitSHA), changed = $(SHAChanged)" />
</Target>
I'm trying to create a reusable Target in msbuild, following the basic model outlined in How to invoke the same msbuild target twice?
I'm stuck trying to pass a property that I want interpreted as a list. I haven't found an example online that deals with this situation. As I understand it, the problem is that Properties is already treated as a list item, so it doesn't like having a list item passed in as well. Is there a way to get msbuild to pack and unpack the list correctly here?
Msbuild is complaining with:
error MSB4012: The expression "FilesToZip=#(Scripts)" 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.
Here's an example caller:
<Target Name="BuildMigrationZip">
<MSBuild Projects="BuildZip.msbuild"
Targets="BuildZip"
Properties="FilesToZip=#(Scripts);OutputZipFile=$(MigrationPackageFilePath);OutputFolder=$(MigrationPackagePath);Flatten=true"/>
<Message Text="Created database migration zip: $(MigrationPackageFilePath)" Importance="high"/>
</Target>
And the base target:
<Target Name="BuildZip">
<MakeDir Directories="$(OutputFolder)"/>
<Zip Files="#(FilesToZip)"
ZipFileName="$(OutputZipFile)"
Flatten="$(Flatten)"
ParallelCompression="false" />
</Target>
I'm basically at the point of just going back to cut and paste for these, although I want to package up a number of zips here.
UPDATE: The same issue applies to setting Inputs on the reusable target. My question up to this point addresses the raw functionality, but it would be nice to keep dependencies working. So for example:
<Target Name="BuildZip"
Inputs="#(FilesToZip)"
Outputs="$(OutputZipFile)">
<MakeDir Directories="$(OutputFolder)"/>
<Zip Files="#(FilesToZip)"
ZipFileName="$(OutputZipFile)"
Flatten="$(Flatten)"
ParallelCompression="false" />
</Target>
They key is to pass the list around as a property. So when your Scripts list is defined as
<ItemGroup>
<Scripts Include="A"/>
<Scripts Include="B"/>
<Scripts Include="C"/>
</ItemGroup>
then you first convert it into a property (which just makes semicolon seperated items, but msbuild knows how to pass this via the Properties of the MSBuild target) then pass it to the target:
<Target Name="BuildMigrationZip">
<PropertyGroup>
<ScriptsProperty>#(Scripts)</ScriptsProperty>
</PropertyGroup>
<MSBuild Projects="$(MSBuildThisFile)" Targets="BuildZip"
Properties="FilesToZip=$(ScriptsProperty)" />
</Target>
(note I'm using $(MSBuildThisFile) here: you don't necessarily need to create seperate build files for every single target, in fact for small targets like yours it's much more convenient to put it in the same file)
Then in your destination target you turn the property into a list again:
<Target Name="BuildZip">
<ItemGroup>
<FilesToZipList Include="$(FilesToZip)"/>
</ItemGroup>
<Message Text="BuildZip: #(FilesToZipList)" />
</Target>
Output:
BuildZip: A;B;C
Update
When working with Inputs, you cannot pass #(FilesToZip) since that expands to nothing because is not a list: it's a property - which happens to be a number of semicolon-seperated strings. And as such, it is usable for Inputs you just have to expand it as the property it is i.e. $(FilesToZip):
<Target Name="BuildZip"
Inputs="$(FilesToZip)"
Outputs="$(OutputZipFile)">
...
</Target>
Output of second run:
BuildZip:
Skipping target "BuildZip" because all output files are up-to-date with respect to the input files.
Is it possible using default MSBuild technology to access a listing within an item group as a property in msbuild? I know I can do this in a custom task in C#, but I am trying to use built-in capabilities if possible.
Example:
I have an item group:
<ItemGroup>
<SolutionToBuild Include="$(SolutionRoot)\Solutions\ClassLib\ClassLib.sln">
<Properties>
AssemblySigningKey=MySigningKey;
OutDir=$(BinariesRoot)\SomeLocation\;
LibraryName=ClassLib;
PlatformTarget=x86;
</Properties>
</SolutionToBuild>
<SolutionToBuild Include="$(SolutionRoot)\Solutions\BLAH\BLAH.sln">
<Properties>
ProjectType=Web;
</Properties>
</SolutionToBuild>
</ItemGroup>
I would like to extract the value of AssemblySigningKey, if it exists, and place this value into an MSBuild variable.
I have tried a few methods and the closest example I could find is using a tranformation within a separate target, but even this looks to be a bit of a hack, even if I could get the Condition to work I would then have to parse out the value splitting on the =. Is there no standard method to access this metadata within the item group?
<Target Name="TransformProps"
Inputs="%(SolutionToBuild.Identity)"
Outputs="_Non_Existent_Item_To_Batch_">
<PropertyGroup>
<IncludeProps>%(SolutionToBuild.Properties)</IncludeProps>
</PropertyGroup>
<ItemGroup>
<IncludeProps Include="$(IncludeProps)" />
<Solution Include="#(SolutionToBuild)">
<IncludeProps Condition="'True'=='True' ">#(IncludeProps ->'-PROP %(Identity)', ' ')</IncludeProps>
</Solution>
</ItemGroup>
</Target>
My main target would call into the tranform in the following manner:
<Target Name="Main" DependsOnTargets="TransformProps">
<Message Text="Solution info: %(Solution.Identity) %(Solution.IncludeProps)" />
</Target>
Items Metadata are declared and transformed using xml tags. It seems like you're using the MSBuild Task to build some solutions - the properties tag is a parameter specific to this task.
The conversion from comma separated list and items as you tried won´t help because, as you mentioned, you still have the equal sign as the link from the keys to the values. I think there´s no way of obtaining the signing key value without parsing. After all msbuild do not consider the list of properties as metadata, it is just a list of strings.
I did the script below to exemplify how msbuild declare and read metadata. It is not an option for you because your ItemGroup structure cannot be changed.
IMHO in this case you have no option but use a custom task and do the parsing. Use Inline Tasks if you´re building with msbuild 4.0.
<?xml version="1.0" encoding="UTF-8" ?>
<Project DefaultTargets="Main" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<SolutionToBuild Include="$(SolutionRoot)\Solutions\ClassLib\ClassLib.sln">
<AssemblySigningKey>MySigningKey123</AssemblySigningKey>
<Properties>
AssemblySigningKey=MySigningKey456;
OutDir=$(BinariesRoot)\SomeLocation\;
LibraryName=ClassLib;
PlatformTarget=x86;
</Properties>
</SolutionToBuild>
</ItemGroup>
<Target Name="TransformProps">
<PropertyGroup>
<MySigningKey>#(SolutionToBuild->'%(AssemblySigningKey)')</MySigningKey>
</PropertyGroup>
</Target>
<Target Name="Main" DependsOnTargets="TransformProps">
<Message Text="My desired Property Value: $(MySigningKey)" />
</Target>
I'm trying to output the variable from one target, into the parent target which started it. For example,
Target 1 simply calls the task in file 2 and is supposed to be able to use the variable set within that. However, I just can't seem to get it to work (wrong syntax perhaps?). Target 1 looks like this:
<Target Name="RetrieveParameter">
<MSBuild Projects="$(MSBuildProjectFile)" Targets="ObtainOutput" />
<Message Text="Output = $(OutputVar)" />
</Target>
Target 2 is where it reads in the value of the text file and sets it to the property and sets the variable 'OutputVar' to match. This is supposed to be returned to the parent.
<Target Name="ObtainOutput" Outputs="$(OutputVar)">
<ReadLinesFromFile File="output.txt">
<Output TaskParameter="Lines"
PropertyName="OutputVar" />
</ReadLinesFromFile>
</Target>
I'm quite new to MSBuild tasks, so it could well be something obvious. All I want to do is set a variable in one task, and then have that available in the parent task which called it.
Julien has given you the right answer, but not explained why it is correct.
As you're new to MSBuild tasks, I'll explain why Julien's answer is correct.
All tasks in MSBuild have parameters - you will know them as the attributes that you put on the task. Any of these parameters can be read back out by placing an Output element within it. The Output element has three attributes that can be used:
TaskParameter - this is the name of the attribute/parameter on the task that you want to get
ItemName - this is the itemgroup to put that parameter value into
PropertyName - this is the name of the property to put that parameter value into
In your original scripts, you were invoking one from the other. The second script will execute in a different context, so any property or itemgroup it sets only exists in that context. Therefore when the second script completes, unless you have specified some Output elements to capture values they will be discarded.
Note that you can put more than one Output element under a task to capture multiple parameters or just set the same value to multiple properties/itemgroups.
You have to use TargetOutputs of the MSBuild task:
<Target Name="RetrieveParameter">
<MSBuild Projects="$(MSBuildProjectFile)" Targets="ObtainOutput">
<Output TaskParameter="TargetOutputs" ItemName="OutputVar"/>
</MSBuild>
<Message Text="Output = #(OutputVar)" />
</Target>
(More information on MSBuild task.)
I have a script that attempts to construct an ItemGroup out of all files in a certain directory while excluding files with certain names (regardless of extension).
The list of files to be excluded initially contains file extensions, and I am using Community Tasks' RegexReplace to replace the extensions with an asterisk. I then use this list in the item's Exclude attribute. For some reason the files do not get excluded properly, even though the list appears to be correct.
To try and find the cause I created a test script (below) which has two tasks: first one initialises two properties with the list of file patterns in two different ways. The second task prints both properties and the files resulting from using both these properties in the Exclude attribute.
The properties' values appear to be identical, however the resulting groups are different. How is this possible?
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTargets="Init;Test" ToolsVersion="3.5">
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<Target Name="Init">
<ItemGroup>
<OriginalFilenames Include="TestDir\SampleProj.exe"/>
<OriginalFilenames Include="TestDir\SampleLib1.dll"/>
</ItemGroup>
<RegexReplace Input="#(OriginalFilenames)" Expression="\.\w+$" Replacement=".*">
<Output TaskParameter="Output" ItemName="PatternedFilenames"/>
</RegexReplace>
<PropertyGroup>
<ExcludeFilesA>TestDir\SampleProj.*;TestDir\SampleLib1.*</ExcludeFilesA>
<ExcludeFilesB>#(PatternedFilenames)</ExcludeFilesB>
</PropertyGroup>
</Target>
<Target Name="Test">
<Message Text='ExcludeFilesA: $(ExcludeFilesA)' />
<Message Text='ExcludeFilesB: $(ExcludeFilesB)' />
<ItemGroup>
<AllFiles Include="TestDir\**"/>
<RemainingFilesA Include="TestDir\**" Exclude="$(ExcludeFilesA)"/>
<RemainingFilesB Include="TestDir\**" Exclude="$(ExcludeFilesB)"/>
</ItemGroup>
<Message Text="
**AllFiles**
#(AllFiles, '
')" />
<Message Text="
**PatternedFilenames**
#(PatternedFilenames, '
')" />
<Message Text="
**RemainingFilesA**
#(RemainingFilesA, '
')" />
<Message Text="
**RemainingFilesB**
#(RemainingFilesB, '
')" />
</Target>
</Project>
Output (reformatted somewhat for clarity):
ExcludeFilesA: TestDir\SampleProj.*;TestDir\SampleLib1.*
ExcludeFilesB: TestDir\SampleProj.*;TestDir\SampleLib1.*
AllFiles:
TestDir\SampleLib1.dll
TestDir\SampleLib1.pdb
TestDir\SampleLib2.dll
TestDir\SampleLib2.pdb
TestDir\SampleProj.exe
TestDir\SampleProj.pdb
PatternedFilenames:
TestDir\SampleProj.*
TestDir\SampleLib1.*
RemainingFilesA:
TestDir\SampleLib2.dll
TestDir\SampleLib2.pdb
RemainingFilesB:
TestDir\SampleLib1.dll
TestDir\SampleLib1.pdb
TestDir\SampleLib2.dll
TestDir\SampleLib2.pdb
TestDir\SampleProj.exe
TestDir\SampleProj.pdb
Observe that both ExcludeFilesA and ExcludeFilesB look identical, but the resulting groups RemainingFilesA and RemainingFilesB differ.
Ultimately I want to obtain the list RemainingFilesA using the pattern generated the same way ExcludeFilesB is generated. Can you suggest a way, or do I have to completely rethink my approach?
The true cause of this was revealed accidentally when a custom task threw an exception.
The actual value of ExcludeFilesA is TestDir\SampleProj.*;TestDir\SampleLib1.* like one might expect. However the actual value of ExcludeFilesB is TestDir\SampleProj.%2a;TestDir\SampleLib1.%2a.
Presumably Message unescapes the string before using it, but Include and Exclude do not. That would explain why the strings look the same but behave differently.
Incidentally, the execution order doesn't seem to have anything to do with this, and I'm pretty sure (following extensive experimentation) that everything gets executed and evaluated exactly in the order in which it appears in this script.
ItemGroups need to be evaluated before targets execution, and the PatternedFilenames ItemGroup is being created on the fly within its target container.
You could workaround this using the CreateItem task, which will ensure the PatternedFilenames scope throughout the execution:
<RegexReplace Input="#(OriginalFilenames)" Expression="\.\w+$" Replacement=".*">
<Output TaskParameter="Output" ItemName="PatternedFilenames_tmp"/>
</RegexReplace>
<CreateItem Include="#(PatternedFilenames_tmp)">
<Output TaskParameter="Include" ItemName="PatternedFilenames"/>
</CreateItem>