Merge Property and ItemGroup that are unrelated into a single Property - msbuild-propertygroup

I have a Property and an ItemGroup and I need to merge them into a single Property with a 1 to 1 relationship:
<PropertyGroup>
<Servers>SvrA,SvrB,SvrC<Servers>
<PropertyGroup>
<ItemGroup>
<--Get full folder paths-->
<ConfigFolders Include="$([System.IO.Directory]::GetDirectories('C:\02-App', 'Folder-?'))"/>
<--Get just the folder names: 'Folder-1;Folder-2;Folder-3'-->
<ConfigFolderNames Include="$([System.IO.Path]::GetFileName('%(ConfigFolders.FullPath)'))" />
</ItemGroup>
What do I need to do to get the end result like this?
<PropertyGroup>
<ServerList>SvrA,Folder-1;SvrB,Folder-2;SvrC,Folder-3</ServerList>
<PropertyGroup>

Related

MSBuild conditions based on imported properties

I have a solution that has multiple projects, and I'd like them all to be able to access a set of shared properties that have been defined once in a common file at the solution level.
This mostly works fine using the code below and I can use the imported properties in the BeforeBuild target, however the problem I'm having is that I can't use the imported properties in conditions.
So I have the following in a CommonSettings.targets file in the solution folder:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="CommonSettingsTarget">
<PropertyGroup>
<MyCustomProperty>Sample</MyCustomProperty>
</PropertyGroup>
</Target>
</Project>
In my project file I have:
<Project ToolsVersion="14.0" DefaultTargets="Build" InitialTargets="CommonSettingsTarget" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(SolutionDir)CommonSettings.targets" />
<ItemGroup>
<EmbeddedResource Include="EmbeddedDocument.txt" Condition=" '$(MyCustomProperty)' == 'Sample' " />
</ItemGroup>
<Target Name="BeforeBuild">
<Message Text="MyCustomProperty='$(MyCustomProperty)'" Importance="high" />
</Target>
</Project>
In the above, I can see my imported property displayed in the output window as "MyCustomProperty='Sample'" which is great, however when it's used as part of a condition (to optionally include an embedded resource), the condition is never satisfied.
Is there any way to make the imported properties work with conditions?
As your ItemGroup is not within a target, but the PropertyGroup is, the CommonSettingsTarget has not yet been executed when your condition is evaluated and thus MyCustomProperty has not yet been defined.
The Message task is called from within the BeforeBuild target which depends on CommonSettingsTarget and thus MyCustomProperty has been defined when you create the message.
Think of the Import as copying the imported project into your project file. The result would be something like this:
<Project ToolsVersion="14.0" DefaultTargets="Build" InitialTargets="CommonSettingsTarget" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="CommonSettingsTarget">
<PropertyGroup>
<MyCustomProperty>Sample</MyCustomProperty>
</PropertyGroup>
</Target>
<ItemGroup>
<EmbeddedResource Include="EmbeddedDocument.txt" Condition=" '$(MyCustomProperty)' == 'Sample' " />
</ItemGroup>
<Target Name="BeforeBuild">
<Message Text="MyCustomProperty='$(MyCustomProperty)'" Importance="high" />
</Target>
</Project>
This is what happens:
You define a target CommonSettingsTarget which will define MyCustomProperty when it is executed. Not now.
You define the ItemGroup and therefore evaluate the condition. It returns false, because MyCustomProperty has not yet been defined.
You define a target BeforeBuild.
You run the initial target, i.e. CommonSettingsTarget. Now MyCustomProperty is defined.
You run the default target which depends on BeforeBuild and thus runs BeforeBuild. There you evaluate MyCustomProperty which has been defined in step 4.
As a solution, remove the CommonSettingsTarget target and define the PropertyGroup as a child of the Project in CommonSettings.targets instead:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MyCustomProperty>Sample</MyCustomProperty>
</PropertyGroup>
</Project>
In your project file, you should remove the InitialTargets="CommonSettingsTarget" then.

Custom property on projectfile preset by solution

I'm having a custom property on my project to build the same app with different ressources (images).
project.jsproj
<ItemGroup>
<Content Condition="$(Customization) == ''" Include="images\uwp\*.png" />
<Content Condition="$(Customization) != ''" Include="images\$(Customization)\uwp\*.png" />
</ItemGroup>
this works fine via msbuild:
msbuild project.jsproj /property:Configuration=Release;Platform=x64;Customization=theme_xy
My question is if there is a possibility to preset this custom property on a solution on VisualStudio that will be applied on a build there as well.
For example:
a) Solution1.sln embedds project.jsproj with Customization property empty
b) Solution2.sln embedds project.jsproj with Customization property = "theme_xy"
Any help is appreciated - thanks
if there is a possibility to preset this custom property on a solution on VisualStudio that will be applied on a build there as well.
The answer is yes, but the conditional limit is that you could not use the same project.jsproj file in Solution1.sln and Solution2.sln. You can set a PropertyGroup in the project.jsproj file in Solution1.sln:
<PropertyGroup>
<Customization></Customization>
</PropertyGroup>
<ItemGroup>
<Content Condition="$(Customization) == ''" Include="images\uwp\*.png" />
<Content Condition="$(Customization) != ''" Include="images\$(Customization)\uwp\*.png" />
</ItemGroup>
That is equivalent to change the project.jsproj file in solution1.sln:
<ItemGroup>
<Content Include="images\uwp\*.png" />
</ItemGroup>
In the Solution2.sln, you need to change the project.jsproj file:
<PropertyGroup>
<Customization>theme_xy</Customization>
</PropertyGroup>
But if you want use the same project.jsproj in the solution1.sln and solution2.sln without any other extra changes, you still need set Condition for the PropertyGroup and this Condition need to be transferred from outside of VS, like command line. In this case, you could not embeds the same project.jsproj with conditional custom properties in different solutions.
<PropertyGroup Condition="$(Customization) == ''">
<Customization></Customization>
</PropertyGroup>
Solved this problem by differentiation of Solution name:
<PropertyGroup>
<Customization></Customization>
</PropertyGroup>
<PropertyGroup Condition="'$(SolutionName)' == 'Solution1'">
<Customization>theme_xy</Customization>
</PropertyGroup>

Remove project reference from command line

Is it possible to remove a project reference from the command line? And if so, how? Imagine I have two projects in a solution: WPF project A, and Class Library B. A has a project reference to B so that it will depend on the output of project B. Now I wan't to remove the project reference from the command line as to be able to automate in our build. Looking as the .csproj file the project reference looks something like this.
<ProjectReference Include="..\B\B.csproj">
<Project>{7B68745C-382E-4272-897D-123A0AD80391}</Project>
<Name>B</Name>
</ProjectReference>
ProjectReference is an item, you can add the Item to be conditionally excluded inside a conditioned ItemGroup:
<PropertyGroup>
<ExcludeReference Condition="'$(ExcludeReference)'==''">false</ExcludeReference>
</PropertyGroup>
<ItemGroup Condition="'$(ExcludeReference)'=='true'">
<ProjectReference Include="..\B\B.csproj">
<Project>{7B68745C-382E-4272-897D-123A0AD80391}</Project>
<Name>B</Name>
</ProjectReference>
</ItemGroup>
From the command line you can pass:
MsBuild SomeProject.proj /p:ExcludeReference=true
UPDATE:
You can have your optional reference in a separate project and import it:
ConditionalReferences.proj
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectReference Include="..\B\B.csproj">
<Project>{7B68745C-382E-4272-897D-123A0AD80391}</Project>
<Name>B</Name>
</ProjectReference>
</ItemGroup>
</Project>
And in your .csproj
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ExcludeReference Condition="'$(ExcludeReference)'==''">false</ExcludeReference>
</PropertyGroup>
<Import Project="ConditionalReferences.proj" Condition="'$(ExcludeReference)'=='false'"/>
</Project>

How to modify the output path in the PreBuildEvent or BeforeBuild

I am using the following code in the BeforeBuild event:
<XmlPeek XmlInputPath="SiteSettings.config" Query="appSettings/add[#key='cProjectNumber']/#value">
<Output TaskParameter="Result" ItemName="value" />
</XmlPeek>
<PropertyGroup>
<ProjectNumber>0#(value)</ProjectNumber>
</PropertyGroup>
which then gives me access to $(ProjectNumber) which will contain a project number like 1234. I then use this to include some project specific overrides on classes. This is working fine.
My issue is accessing ProjectNumber outside of the BeforeBuild event.
What I would like to do is to modify the <OutputPath>bin\</OutputPath> of the project so it goes into a project specific folder (so I know which builds are done). I tried <OutputPath>bin\$(ProjectNumber)\</OutputPath> but I assume my property is out of scope as it's empty.
Does anyone know of a way to achieve what I want?
EDIT: This is what I have tried with declaring a global var:
<Project ToolsVersion="4.0" DefaultTarget="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectNumber></ProjectNumber>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\$(ProjectNumber)\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<Target Name="BeforeBuild">
<XmlPeek XmlInputPath="SiteSettings.config" Query="appSettings/add[#key='cProjectNumber']/#value">
<Output TaskParameter="Result" ItemName="value" />
</XmlPeek>
<PropertyGroup>
<ProjectNumber>0#(value)</ProjectNumber>
</PropertyGroup>
<ItemGroup>
<Compile Include="Accounts\$(ProjectNumber)\Objects\MyObject.cs">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Target>
<Target Name="AfterBuild">
<Message Text="Proj:$(ProjectNumber)" Importance="High" />
</Target>
</Project>
AfterBuild has the correct project number message printed but <OutputPath>bin\$(ProjectNumber)\</OutputPath> doesn't have the correct value.
You can set a Property in the Project Global level with the name ProjectNumber and empty value as a place holder. The the ProjectNumber Property value you set in the InitializeBuild target will set the global level property of ProjectNumber.
By looking at the sample below you can understand it better:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" InitialTargets="InitializeBuild" DefaultTargets="All" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectNumber></ProjectNumber>
</PropertyGroup>
<Target Name="All">
<Message Text="This is the value after I set it in InitializeBuild '$(ProjectNumber)'"/>
</Target>
<Target Name="BeforeBuild">
<Message Text="This is the value before I set it in InitializeBuild '$(ProjectNumber)'"/>
<PropertyGroup>
<ProjectNumber>12345678</ProjectNumber>
</PropertyGroup>
</Target>
</Project>
In this sample, we are setting Property ProjectNumber with empty value at Global level and then in InitializeBuild target, it is set with value 12345678 as InitializeBuild is part of the InitialTargets and then through DefaultTargets later, once it is accessed in target All it is having the value 12345678.
The output of the above code goes as below:
>msbuild mybuild.msbuild
Microsoft (R) Build Engine version 4.0.30319.17929
[Microsoft .NET Framework, version 4.0.30319.17929]
Copyright (C) Microsoft Corporation. All rights reserved.
Build started 9/25/2013 5:35:22 AM.
Project "D:\CodeBase\COLNEW\COL-BIS\dev\mybuild.msbuild" on node 1 (default targets).
InitializeBuild:
This is the value before I set it in InitializeBuild''
All:
This is the value after I set it in InitializeBuild'12345678'
Done Building Project "D:\CodeBase\COLNEW\COL-BIS\dev\mybuild.msbuild" (default targets).
So your issue can be solved in similar fashion.

How to use the new VS 2010 configuration transforms and apply them to other .config files?

I have setup some configuration transforms in my web.config for my connectionStrings, etc. But I have separated out some areas of my web.config into separate files, ex) appSettings.config.
How can I configure Visual Studio and MSBuild to perform config transformations on these additional config files?
I have already followed the approach of the web.config to relate the files together within my web application project file, but transformations are not automatically applied.
<ItemGroup>
<Content Include="appSettings.Debug.config">
<DependentUpon>appSettings.config</DependentUpon>
</Content>
</ItemGroup>
By default the target managing the transformation (TransformWebConfig) works only on web.config file.
To make it work on your appSettings.config file you'll have to :
Set the Build Action of your file to Content
Call the MSBuild target TransformWebConfig with ProjectConfigFileName=appSettings.config and Configuration=$(Configuration).
To call MSBuild TransformWebConfig target for appSettings.config just after the transformation of web.config files, you need to add this at the end of your project file :
<PropertyGroup>
<!-- Name of your custom config file -->
<ConfigFileName>appSettings.config</ConfigFileName>
</PropertyGroup>
<PropertyGroup>
<!--
This property is used to handle circular dependency between
TransformWebConfig and our custom target TransformAppConfig
-->
<FirstRun Condition="$(FirstRun) == ''">true</FirstRun>
</PropertyGroup>
<!-- This target will be called one time after a call to TransformWebConfig -->
<Target Name="TransformAppConfig"
AfterTargets="TransformWebConfig"
Condition="$(FirstRun) == 'true'">
<MSBuild Projects="$(MSBuildProjectFile)"
Targets="TransformWebConfig"
Properties="ProjectConfigFileName=$(ConfigFileName);
Configuration=$(Configuration);
FirstRun=false"/>
</Target>
<!--
This target will be called one time before PreAutoParameterizationWebConfigConnectionStrings
to add $(ConfigFileName) to autoparameterization step
-->
<Target Name="AddToAutoParameterizationStep"
BeforeTargets="PreAutoParameterizationWebConfigConnectionStrings">
<ItemGroup>
<_WebConfigsToAutoParmeterizeCS Include="#(FilesForPackagingFromProject)"
Condition="('%(FilesForPackagingFromProject.Filename)%(FilesForPackagingFromProject.Extension)'=='$(ConfigFileName)') And !%(FilesForPackagingFromProject.Exclude)">
<TransformOriginalFile>$(AutoParameterizationWebConfigConnectionStringsLocation)\original\%(DestinationRelativePath)</TransformOriginalFile>
<TransformOutputFile>$(AutoParameterizationWebConfigConnectionStringsLocation)\transformed\%(DestinationRelativePath)</TransformOutputFile>
<TransformScope>$(_PackageTempDir)\%(DestinationRelativePath)</TransformScope>
</_WebConfigsToAutoParmeterizeCS>
<_WebConfigsToAutoParmeterizeCSOuputFiles Include="#(_WebConfigsToAutoParmeterizeCS->'%(TransformOutputFile)')">
</_WebConfigsToAutoParmeterizeCSOuputFiles>
</ItemGroup>
</Target>
Something that makes this a lot easier, take a look at the SlowCheetah VS add-in at ... visualstudiogallery
Here is the code that works for me:
<PropertyGroup>
<!-- Name of your custom config file -->
<ConfigFileName>ConnectionStrings.config</ConfigFileName>
<ConfigTransformFileName>ConnectionStrings.$(Configuration).config</ConfigTransformFileName>
</PropertyGroup>
<PropertyGroup>
<!--
This property is used to handle circular dependency between
TransformWebConfig and our custom target TransformAppConfig
-->
<FirstRun Condition="$(FirstRun) == ''">true</FirstRun>
</PropertyGroup>
<Target Name="AddConfigToTransform" AfterTargets="CollectWebConfigsToTransform">
<ItemGroup>
<WebConfigsToTransform Include="#(FilesForPackagingFromProject)" Condition="'%(FilesForPackagingFromProject.Filename)%(FilesForPackagingFromProject.Extension)'=='$(ConfigFileName)'">
<TransformFile>%(RelativeDir)$(ConfigTransformFileName)</TransformFile>
<TransformOriginalFile>$(TransformWebConfigIntermediateLocation)\original\%(DestinationRelativePath)</TransformOriginalFile>
<TransformOutputFile>$(TransformWebConfigIntermediateLocation)\transformed\%(DestinationRelativePath)</TransformOutputFile>
<TransformScope>$([System.IO.Path]::GetFullPath($(_PackageTempDir)\%(DestinationRelativePath)))</TransformScope>
</WebConfigsToTransform>
</ItemGroup>
</Target>
<!--
This target will be called one time before PreAutoParameterizationWebConfigConnectionStrings
to add $(ConfigFileName) to autoparameterization step
-->
<Target Name="AddToAutoParameterizationStep" BeforeTargets="PreAutoParameterizationWebConfigConnectionStrings">
<ItemGroup>
<_WebConfigsToAutoParmeterizeCS Include="#(FilesForPackagingFromProject)" Condition="('%(FilesForPackagingFromProject.Filename)%(FilesForPackagingFromProject.Extension)'=='$(ConfigFileName)') And !%(FilesForPackagingFromProject.Exclude)">
<TransformOriginalFile>$(AutoParameterizationWebConfigConnectionStringsLocation)\original\%(DestinationRelativePath)</TransformOriginalFile>
<TransformOutputFile>$(AutoParameterizationWebConfigConnectionStringsLocation)\transformed\%(DestinationRelativePath)</TransformOutputFile>
<TransformScope>$(_PackageTempDir)\%(DestinationRelativePath)</TransformScope>
</_WebConfigsToAutoParmeterizeCS>
<_WebConfigsToAutoParmeterizeCSOuputFiles Include="#(_WebConfigsToAutoParmeterizeCS->'%(TransformOutputFile)')">
</_WebConfigsToAutoParmeterizeCSOuputFiles>
</ItemGroup>
</Target>

Resources