Using Item functions on metadata values - msbuild

Background: I manage a fairly large solution. Every so often, people add a DLL reference to a project in the solution where they should've added a project reference. I want to issue a warning in such case. I want to do it by finding all reference with 'bin\debug' in their HintPath*. I know that references are Items in ItemGroup, with metadata "HintPath".
I expected something like this to work:
<Warning Text="Reference %(Reference.Identity) should be a project reference. HintPath: %(Reference.HintPath)"
Condition="%(Reference.HintPath).IndexOf('bin\debug') != -1"/>
However, Seems like I can't use string function IndexOf like that. I tried many permutations of the above, without success.
Edit: I know this check is not full-proof, but I just want to reduce honest mistakes.

Using MSBuild 4.0 Property Functions it is possible to do string comparisons:
<Target Name="AfterBuild">
<Message Text="Checking reference... '%(Reference.HintPath)'" Importance="high" />
<Warning Text="Reference %(Reference.Identity) should be a project reference. HintPath: %(Reference.HintPath)"
Condition="$([System.String]::new('%(Reference.HintPath)').Contains('\bin\$(Configuration)'))" />
</Target>

First not that your syntax is not correct for invoking functions, it would need to be:
%(Reference.HintPath.IndexOf(...)) # Note: not supported by MSBuild
However, property functions in MSBuild are not allowed on item metadata, so that is not going to help you either.
What you could work around this, by invoking a separate target which is basically called for every item.
<Target Name="CheckProjectReferences">
<MSBuild
Projects="$(MSBuildProjectFullPath)"
Properties="Identity=%(Reference.Identity);HintPath=%(Reference.HintPath)"
Targets="_Warn"/>
</Target>
<Target Name="_Warn">
<Warning Text="Reference $(Identity) should be a project reference. HintPath: $(HintPath)"
Condition="$(HintPath.IndexOf('bin\debug')) != -1"/>
</Target>
Frankly, I'm not sure if that is enough to catch all "violations". For example, the above will only work for bin\debug, but not for bin\Debug or other mixed-cased variations, which are functionally equivalent. To look for them as well, you'd need to call the IndexOf(string, StringComparison) overload, however just doing:
$(HintPath.IndexOf('bin\debug', System.StringComparison.OrdinalIgnoreCase))
Will not work, because the MSBuild overload resolution will pick IndexOf(char, Int32) and give you this error:
MSB4184: The expression ""bin\debug".IndexOf(bin\debug, System.StringComparison.OrdinalIgnoreCase)" cannot be evaluated. String must be exactly one character long.
So, you'll need to convince it by using the IndexOf(String, Int32, Int32, StringComparison) overload directly:
$(HintPath.IndexOf('bin\debug', 0, 9, System.StringComparison.OrdinalIgnoreCase))
You may need to also check for bin\Release or other variations. I'm not sure if that is the best way to figure out a reference should be a project reference, but if you know (and to a certain extend control) your environment it might be feasible.

#Christian.K is right in his analysis. Another solution would be to force the overload of type string using " for the quotes:
<Warning
Text="..."
Condition="$(HintPath.IndexOf("bin\debug", System.StringComparison.OrdinalIgnoreCase)) != -1" />

Related

Hexadecimal numbers in static method calls

Is it possible to somehow use hexadecimal numbers in a static method call in MSBuild? I've tried a few different variations, but I'm getting this error:
error MSB4186: Invalid static method invocation syntax:
"[MSBuild]::BitwiseAnd(0x1, $(FxCopExit))". Input string was not in a
correct format. Static method invocation should be of the form:
$([FullTypeName]::Method()), e.g. $([System.IO.Path]::Combine('a', 'b')).
I'm trying to do this:
<Warning Text="Assembly loading exception" Condition="$([MSBuild]::BitwiseAnd(0x8, $(FxCopExit))) == 0x8"/>
It's worth noting that the 0x8 on the right hand side of the condition is acceptable, so if I instead do
<Warning Text="Assembly loading exception" Condition="$([MSBuild]::BitwiseAnd(8, $(FxCopExit))) == 0x8"/>
Everything works out fine. Since the error codes I'm mapping are defined in hexadecimal, I'd much prefer to keep them in hex here as well, for future readers of the script...
As far as I can find out, it is not possible. The only way to keep the hexadecimal is to wrap it in a conversion:
<Warning Text="Assembly loading exception" Condition="$([MSBuild]::BitwiseAnd($([System.Convert]::ToInt32(`0x8`, 16)), $(FxCopExit))) == 0x8"/>
I'm not sure if this is actually better/more readable than just rewriting the number in decimal and documenting, but this is technically correct, at least...

Using MSBuild PropertyGroup outside of Target block

I have a project files collection:
<ItemGroup>
<ApplicationToDeploy
Include="Frontend.WebSite.csproj;11.WebServices.csproj;22.WebServices.csproj"/>
<ApplicationToDeploy
Include="33.WebServices.csproj;44.WebServices.csproj;Workflow55Svc.csproj"/>
</ItemGroup>
I'm trying to get collection of .config-files of these projects:
<Target Name="111">
<PropertyGroup>
<Cfgs>#(ApplicationToDeploy->'%(RootDir)%(Directory)*.config')</Cfgs>
</PropertyGroup>
<ItemGroup>
<InputConfigs Include="$(Cfgs)" />
</ItemGroup>
<Message Text="Cfgs: #(InputConfigs)"/>
</Target>
Inside the Target block all works fine (I see collection of Web.Configs, App.Configs, Log4net.Configs etc.):
Cfgs: C:\Sources\WebServices\11\WebServices\11.WebServices\Web.config;C:\Sources\WebServices\22\WebServices\22.WebServices\web.log4net.config;C:\Sources\WebServices\33\WebServices\33.WebServices\web.environment.config
But I want to initialize this ItemGroup outside of the Target block. Like this:
<PropertyGroup>
<Cfgs>#(ApplicationToDeploy->'%(RootDir)%(Directory)*.config')</Cfgs>
</PropertyGroup>
<ItemGroup>
<InputConfigs Include="$(Cfgs)" />
</ItemGroup>
<Target Name="111">
<Message Text="Cfgs: #(InputConfigs)"/>
</Target>
And when I do this outside of the Target block I get this:
Cfgs: C:\Sources\WebServices\11\WebServices\11.WebServices\*.config;C:\Sources\WebServices\22\WebServices\22.WebServices\*.config;C:\Sources\WebServices\33\WebServices\33.WebServices\*.config
I don't understand what's happens. Is it possible to get the same result outside Target block?
I don't understand what's happens.
This behavior is an effect of the MSBuild evaluation order:
During the evaluation phase of a build:
Properties are defined and modified in the order in which they
appear. Property functions are executed. Property values in the form
$(PropertyName) are expanded within expressions. The property value
is set to the expanded expression.
Item definitions are defined and modified in the order in which they appear. Property functions have already been expanded within expressions. Metadata values are set to the expanded expressions.
Item types are defined and modified in the order in which they appear. Item values in the form #(ItemType) are expanded. Item transformations are also expanded. Property functions and values have already been expanded within expressions. The item list and metadata values are set to the expanded expressions.
During the execution phase of a build:
Properties and items that are defined within targets
are evaluated together in the order in which they appear. Property
functions are executed and property values are expanded within
expressions. Item values and item transformations are also expanded.
The property values, item type values, and metadata values are set to
the expanded expressions."
There's another key point on that link "(...) The string expansion is dependent on the build phase.".
You're using the property 'Cfgs' to recursively map your projects' folders AND to define a wildcard to config files (*.config). When you define 'Cfgs' INSIDE the target, the InputConfigs receives the expanded value of Cfgs (semicolon-separated string list of folders), and just resolve the wildcards. On the other hand, when you define 'Cfgs' OUTSIDE the target, the InputConfigs receives the unexpanded value of Cfgs (#(ApplicationToDeploy->'%(RootDir)%(Directory)*.cs'). When the InputConfigs expands it, it results on the semicolon-separated string list of folders, but it doesn't resolve the wildcards (*.config).
Is it possible to get the same result outside Target block?
I think that InputConfigs should always receive the expanded list of directories. The expansion is made during the execution phase of the build. During this phase, only
properties and items defined within targets are evaluated. So, I would keep all the initialization inside an 'Initialization' Target block. I don't mean it is impossible to do it outside a Target block, but for the reasons mentioned it does not seems logical. =]
Hope this helps,

Conditions for SetDirectory in Wix

I've been trying to figure out why my condition isn't working for my SetDirectory. I haven't been able to find any examples of anyone actually doing this, only that it is possible to do so, according to the WiX documentation.
I've tried the following:
<SetDirectory Id="INSTALLLOCATION" Value="TEST">ComputerName=LJPRESCOTT1</SetDirectory>
This compiles, but gets ignored at runtime. The files end up getting installed straight on the C:\ Drive, and not in C:\TEST like I'm aiming for.
and
<SetDirectory Id="INSTALLLOCATION" Value="TEST">[ComputerName]=LJPRESCOTT1</SetDirectory>
This doesn't compile and throws a "Bad conditional string" error.
If I do this, it creates a directory named LJPRESCOTT1 as expected:
<SetDirectory Id="INSTALLLOCATION" Value="[ComputerName]" />
So I know the Comp Name is correct.
Am I doing something wrong here, or is this not possible?
Thanks!
You use the syntax [ComputerName] only when you are doing formatting, or using values that are of type Formatted, hence the syntax error in the second example.
But in the first example what you are doing is comparing the value of the ComputerName property with the value of the LJPRESCOTT1 (public) property, which I imagine always evaluates to false. If you want to compare ComputerName to a hard string value you need some quotes:
ComputerName="LJPRESCOTT1"

MSBuild tasks can accept primitive arrays, but how do you write one to pass into the task?

I would guess it has to be an ITaskItem since it's a vector instead of scalar, I've got the only 2 MsBuild books here on my desk, and I can't find examples of how to pass in an array to a task. I want to do a string array, but I'd like to know the proper way that would work with any primitive type.
How do you pass in an array of string (or int) to a MsBuild task?
MSBuild tasks can accept ITaskItem, primitives, string or an array of any of those for parameters. You just declare the type in your task and then the values will be converted before passed to the task. If the value cannot convert to the type then an exception will be raised and the build will be stopped.
For example if you have a task which accepts an int[] named Values then you could do.
<Target Name="MyTarget">
<MyTask Values="1;45;657" />
<!-- or you can do -->
<ItemGroup>
<SomeValues Include="7;54;568;432;79" />
</ItemGroup>
<MyTask Values="#(SomeValues) />
</Target>
Both approaches are essentially the same. The other answers stating that all parameters are strings or that you have to use ITaskItem are incorrect.
You said you have two books on MSBuild, then I presume one is my Inside the Microsoft Build Engine book, you should read the chapter on Custom Tasks so that you get a full grasp on these topics. There is a section explaining parameter types specifically.
IIRC, msbuild items are always string arrays - that is the only option. So an array of integers would be stored as an array numeric strings.

MSBuild: how to control the parsing of a semicolon delimited property

When a single property contains semicolons, MSBuild automatically parse the property into a list of properties when used within an itemgroup. Here's a snippet from my project:
<PropertyGroup>
<ConnectionString>workstation id=.;packet size=4096;Integrated Security=SSPI;data source=.;initial catalog=$(SqlDbName)</ConnectionString>
</PropertyGroup>
<ItemGroup>
<InstallShieldProperties Include="
CONNECTIONSTRING=$(ConnectionString);
Another=$(value)"/>
</ItemGroup>
When a task consumes the #(InstallShieldProperties) itemgroup, MSBuild will parse the ConnectionString property into a list of subset properties since it contains semicolons.
foreach (string property in Properties)
{
// Properties array parsed to pieces
}
I know I can change the delimiter of the itemgroup, but that won't make any difference.
I'm trying to avoid manipulating the string[] array within the custom task.
In MSBuild 4.0, you can use $([MSBuild]::Escape($(ConnectionString))).
AFAICS, you can either escape the semicolon in the $(ConnectionString) property like:
<ConnectionString>workstation id=.%3Bpacket size=4096%3B.."</ConnectionString>
Or use some task to replace the ';' in the ConnectionString property to '%3B' and then use that property in InstallShieldProperties item.
The other way could be to change the property type in the custom task from string[] to string, and then split it yourself, the way you want it. You could use enclosing quotes to separate Connection string part from other key/value pairs.
Or if it makes sense for your custom task, then maybe connection string is a special enough property to have as a separate task property.
In MSBuild 4.0, there are now Property Functions. One thing these allow you to do is call .NET String instance methods directly on your properties as if they are strings (which they are).
In your example, instead of using:
$(ConnectionString)
You could use:
$(ConnectionString.Replace(';', '%3B'))
Which will call the String method Replace() to replace semicolons with %3B