Introduction
This is the second article in the series on how to integrate third party tools into configuration subsystem of Visual Studio. In my previous article, I explained how to create custom property sheets which show up in "Properties" dialog of Visual Studio. In this article, I'll explain how to retrieve setup data and format it according to tool's requirements.
Background
Property sheet is introduced into configuration subsystem by creating XML file containing project schema definition with Rule and Property elements. Each UI control on the sheet will have <...Property>
element defining it. So if your XML looks something like this:
="1.0"="utf-8"
<Rule ...="">
...
<BoolProperty Name="StdCall" DisplayName="__stdcall" ...=""/>
<StringProperty Name="PrecompiledHeaderFile" ...="" />
</Rule>
In your Target, you would have code which would retrieve data from property variables and format it into command line options. May be something like this:
<Target Name="Build">
<PropertyGroup>
<options>/One"$(StdCall)" /Two:$(PrecompiledHeaderFile) /build:$(Configuration) ... </options>
<options>$(options)/Three"$(SomeProp)" /Four:$(SomeOtherProp) ... </options>
</PropertyGroup>
<Exec Command="tool.exe $(options)" ... />
</Target>
This is a straightforward but laborious and not so flexible method. I'd like to show you how this process could be done without any hard coding by using XmlPeek task.
User Interface
When UI property sheet is created (See for more info), you have few options which template to use. If you define property sheet's Rule to use "tool" template:
="1.0"="utf-8"
<Rule Name="ConfigUI"
DisplayName="Sample"
PageTemplate="tool"
xmlns="http://schemas.microsoft.com/build/2009/properties">
<Rule.Categories>
<Category Name="General" DisplayName="General" />
...
<Category Name="Command Line" DisplayName="Command Line" Subtype="CommandLine" />
</Rule.Categories>
<StringProperty Name="AdditionalOptions" DisplayName="Additional Options"
Description="Additional Options." Category="Command Line"
F1Keyword="vc.project.AdditionalOptionsPage" />
</Rule>
Visual Studio will allow you to add special category with Subtype
"CommandLine
":
<Category Name="Command Line" DisplayName="Command Line" Subtype="CommandLine" />
This category opens up a read only view into set of command line options generated from your configuration:
These switches and parameters are created based on attributes you set on individual <...Property>
elements. We will briefly go over these attributes:
IncludeInCommandLine
This Boolean
attribute (See reference) hints to the command-line generator whether to include this property in the command line.
SwitchPrefix
The SwitchPrefix
attribute (See Reference) holds the preamble to the switch such as /
in the example above, generated by the engine. This property could be set on Rule element or any of the property elements.
Switch
The Switch
attribute (See Reference) holds the representation of the switch itself. In the example above, these are SLP
, SLP-V
, SLP-VS
.
ReverseSwitch
The ReverseSwitch attribute (See Reference) is present only on <BoolPreperty...> element and holds representation of the switch when property has value "false"
. Obviously Switch holds representation of value "true"
.
Separator
The Separator attribute (See Reference) holds the token used to separate a switch from its value.
CommandLineValueSeparator
The CommandLineValueSeparator attribute (See Reference) holds the separator used to divide values in list property. So when <StringListProperty ... > holds Val1;Val2;Val3
, if CommandLineValueSeparator is specified as comma, the command line looks like this: /p"Val1","Val2","Val3"
If not specified, the command line looks like this: /p"Val1" /p"Val2" /p"Val3"
. Setting up Separator attribute changes it into: /p:"Val1","Val2","Val3"
and /p:"Val1" /p:"Val2" /p:"Val3"
respectively.
Example
<BoolProperty ReverseSwitch="Zc:wchar_t-"
Name="TreatWChar" Switch="Zc:wchar_t" ...>
<StringProperty Name="ProgramDataBaseFileName" Switch="Fd" ... >
<StringListProperty Name="PreprocessorDefinitions" Switch="D " ...>
...
The combination of the above properties will generate the following command line options:
/Zc:wchar_t- /Fd"Debug\vc120.pdb" /D "WIN32" /D "_DEBUG" /D "_CONSOLE" ...
Generating Command Line
Getting the Properties
The fact that these rules and properties are defined in regular XML files allows us to process them in code. All we have to do is to query the file and get all the properties with IncludeInCommandLine attribute not set to "false
". We could query any XML file by using XmlPeek task. (For detailed reference on how to call tasks, see MSDN.)
<XmlPeek XmlInputPath="sample.xml"
Namespaces="Namespace Prefix='x'
Uri='http://schemas.microsoft.com/build/2009/properties'"
Query="/x:Rule/node()[not(contains(@IncludeInCommandLine, 'false'))]">
<Output TaskParameter="Result" ItemName="Peeked" />
</XmlPeek>
This query will return all the configuration properties in the XML file loaded into Peeked
Item:
<BoolProperty Name="ExpandAttributedSource" DisplayName="Expand Attributed Source" ... />
...
<EnumProperty Name="AssemblerOutput" DisplayName="Assembler Output" ... />
<EnumValue Name="NoListing" Switch="" DisplayName="No Listing" ... />
<EnumValue Name="AssemblyCode" Switch="FA" DisplayName="Assembly-Only Listing" ... />
<EnumValue Name="AssemblyAndMachineCode" Switch="FAc" ... />
<EnumValue Name="AssemblyAndSourceCode" Switch="FAs" ... />
<EnumValue Name="All" Switch="FAcs" DisplayName="Assembly, Machine Code" ... />
</EnumProperty>
...
<StringProperty Name="sampleSProperty" DisplayName="Simple String" ... />
Once we have all the properties loaded into Item, we could do something useful with them.
Processing Properties
We will be using combination of Transforming and Batching to perform parsing of all the relevant attributes and creating final command line options base of these attributes (I will not be covering Transforming and Batching in this article, these concepts are well covered elsewhere). First, we need to parse all the relevant data.
Parsing
Parsing is done by going over each element and analyzing the data. Each element is stored as a simple string
so we will be using Regex.Match to extract values and store them into Metadata for each item. The code should look something like this (note: sample code has been simplified for demonstration purposes):
<ItemGroup >
<Properties Include="@(Peeked -> '%(Identity)')">
<Type>$([Regex]::Match(%(Identity), (?<=<)(.*?)(?=\s) ))</Type>
<Name>$([Regex]::Match(%(Identity), (?<=Name=")(.*?)(?=") ))</Name>
<Prefix>$([Regex]::Match(%(Identity), (?<=SwitchPrefix=")(.*?)(?=") ))</Prefix>
<Switch>$([Regex]::Match(%(Identity), (?<=Switch=")(.*?)(?=") ))</Switch>
<ReverseSwitch>$([Regex]::Match(%(Identity),(?<=ReverseSwitch=")(.*?)(?=") ))</ReverseSwitch>
<Separator>$([Regex]::Match(%(Identity), (?<=\sSeparator=")(.*?)(?=") ))</Separator>
<Divider>$([Regex]::Match(%(Identity), (?<=ValueSeparator=")(.*?)(?=") ))</Divider>
<Children>$([Regex]::Matches(%(Identity), EnumValue\s(.*?)>) )</Children>
<Subtype>$([Regex]::Match(%(Identity), (?<=Subtype=")(.*?)(?=") ))</Subtype>
</Properties>
</ItemGroup>
In this example, we are using construct "@(Peeked -> '%(Identity)')"
to tell MSBuild to create Item called <Properties>
, go over each element in <Peeked>
and add data from it to <Properties> without modifications. We also add Metadata <Type>
, <Name>
, <Prefix>
, etc. to each element in <Properties>
with data parsed from original property elements.
Once it is done, we should have <Properties>
filled with original data and parsed values stored as Metadata.
Generating Options
Now we could go over each individual property type by type, extract data and form output switches applying appropriate formatting. We will be using Item <options>
to collect generated output.
StringProperty
We start with StringProperty
elements:
<options Condition="'%(Properties.Type)' == 'StringProperty'
And '$(%(Properties.Name))'!=''"
Include="%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)'$(%(Properties.Name))'"
/>
In this batch, we go over every StringProperty
element in the properties collection and transform it into
%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)$(%(Properties.Name))
sequence. Note the last element $(%(Properties.Name))
. It contains deference of variable with name held in
%(Properties.Name)
. In other words, it is similar to global['name']
. Using the example shown above, the output will contain:
/Fd"Debug\vc120.pdb"
IntProperty
Transforming IntProperty is very similar to StringProperty with the exception of quote symbols around value.
<options Condition="'%(Properties.Type)' == 'StringProperty'
And '$(%(Properties.Name))'!=''"
Include="%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)$(%(Properties.Name))"
/>
BoolProperty
Transforming BoolProperty
requires two runs. First time for all "true
" values and second for all "false
" values.
<options Condition="'$(%(Properties.Name))'=='true'
And '%(Properties.Type)'=='BoolProperty'"
Include="%(Properties.Prefix)%(Properties.Switch)" />
<options Condition="'$(%(Properties.Name))'=='false'
And '%(Properties.Type)'=='BoolProperty'"
Include="%(Properties.Prefix)%(Properties.ReverseSwitch)" />
At this point, <options>
list should contain:
/Fd"Debug\vc120.pdb"
/Zc:wchar_t-
StringListProperty
Processing of StringListProperty
elements is a bit more trickier. If CommandLineValueSeparator is set, all we have to do is to replace divider with symbol contained in CommandLineValueSeparator (note: sample is simplified):
<options Condition="'%(Properties.Type)' == 'StringListProperty' And
'%(Properties.Divider)'!=''"
Include="%(Properties.Prefix)%(Properties.Preamble)" \
$(Replace($(%(Properties.Name)), ';', '%(Properties.Divider)'))"" />
If CommandLineValueSeparator we need to output separate switch for each list value contained in the variable:
Prefix - Switch - Value1,Value2,Value3
has to become:
Prefix - Switch - Value1
Prefix - Switch - Value2
Prefix - Switch - Value3
We can do it by performing Outer Join on Item's value and the list and use newly generated list to create output:
<list-outer-join Condition="'%(Properties.Type)'=='StringListProperty' And
'%(Properties.CommandLineValueSeparator)'==''"
Include="$(%(Properties.Name))" >
<Prefix>%(Properties.Prefix)%(Properties.Switch)%(Properties.Separator)</Prefix>
</list-outer-join>
<options Include="%(list-outer-join.Prefix)"%(list-outer-join.Identity)"" />
First command creates outer join
and second outputs elements in proper format. After running these lines, the final output collection should contain:
/Fd"Debug\vc120.pdb"
/Zc:wchar_t-
/D "WIN32"
/D "_DEBUG"
/D "_CONSOLE"
DynamicEnumProperty
The DynamicEnumProperty is very similar to StringProperty
. All you have to do is to format prefix, switch and value properly.
EnumProperty
The EnumProperty elements are a little bit tricky. The data held in the property cannot be directly used to generate the switch. It holds name of the child <EnumValue> element which has the switch information. So to get to actual switch values, we have to parse child elements of the EnumProperty.
<enum-values Condition="'%(Properties.Type)'=='EnumProperty'"
Include="%(Properties.Children)" >
<Name>%(Properties.Name)</Name>
<Prefix>%(Properties.Prefix)</Prefix>
</enum-values>
Now, we can generate command line options stored in EnumProperty elements.
<options Condition="'$(%(enum-values.Name))'=='$([Regex]::Match(%(Identity),
(?<=Name=")(.*?)(?=")))'"
Include="%(enum-values.Prefix)$([Regex]::Match(%(Identity), (?<=Switch=")(.*?)(?=") ))" />
Done!
Item <options>
now contains a list of all the switches we configured in XML file. The final value could be retrieved as @(options, ' ')
to get a string
of options
separated by space.
Implementation
Everything I discussed up to this point is for demonstration purposes only. I've been trying to explain the principle of operation and sequence of events. Also I was trying to demonstrate how MSBuild could be used to implement these sequences.
When it comes to XML processing, nothing beats the XSLT. I've included super efficient (compared to MSBuild) implementation of the same algorithm but using XSLT transform. There is nothing revolutionary about it so I will not be discussing it in details.
Using the Code
The sample file contains MSBuild implementation of the algorithm and should be used for educational purposes only. For use in production environment, download and use ConfGen.targets.
The file contains a single Target called: GetXmlConfg
which performs all the necessary operations.
It requires property PropertyPageSchema
containing path to XML file to be passed when called.
It can also accept optional parameter Name
which specifies name of the Rule
element, if more than one Rule
is defined in the file.
The target could be executed by MSBuild task like this:
<MSBuild Projects="$(MSBuildProject)" Properties="PropertyPageSchema=UI.xml" Targets="GetXmlConfig" >
<Output PropertyName="out-options" TaskParameter="TargetOutputs"/>
</MSBuild>
<Message Text="TargetOutput : $(out-options)" Importance="high"/>
History
- 03/09/2015 - Published
- 03/15/2015 - Updated ConfGen.targets