In MSBuild, can I use the String.Replace function on a MetaData item? - msbuild

In MSBuild v4 one can use functions (like string.replace) on Properties. But how can I use functions on Metadata?
I'd like to use the string.replace function as below:
<Target Name="Build">
<Message Text="#(Files->'%(Filename).Replace(".config","")')" />
</Target>
Unfortunately this outputs as (not quite what I was going for):
log4net.Replace(".config","");ajaxPro.Replace(".config","");appSettings.Replace(".config","");cachingConfiguration20.Replace(".config","");cmsSiteConfiguration.Replace(".config","");dataProductsGraphConfiguration.Replace(".config","");ajaxPro.Replace(".config","");appSettings.Replace(".config","");cachingConfiguration20.Replace(".config","");cmsSiteConfiguration
Any thoughts?

You can do this with a little bit of trickery:
$([System.String]::Copy('%(Filename)').Replace('config',''))
Basically, we call the static method 'Copy' to create a new string (for some reason it doesn't like it if you just try $('%(Filename)'.Replace('.config',''))), then call the replace function on the string.
The full text should look like this:
<Target Name="Build">
<Message Text="#(Files->'$([System.String]::Copy("%(Filename)").Replace(".config",""))')" />
</Target>
Edit: MSBuild 12.0 seems to have broken the above method. As an alternative, we can add a new metadata entry to all existing Files items. We perform the replace while defining the metadata item, then we can access the modified value like any other metadata item.
e.g.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Files Include="Alice.jpg"/>
<Files Include="Bob.not-config.gif"/>
<Files Include="Charlie.config.txt"/>
</ItemGroup>
<Target Name="Build">
<ItemGroup>
<!--
Modify all existing 'Files' items so that they contain an entry where we have done our replace.
Note: This needs to be done WITHIN the '<Target>' (it's a requirment for modifying existing items like this
-->
<Files>
<FilenameWithoutConfig>$([System.String]::Copy('%(Filename)').Replace('.config', ''))</FilenameWithoutConfig>
</Files>
</ItemGroup>
<Message Text="#(Files->'%(FilenameWithoutConfig)')" Importance="high" />
</Target>
</Project>
Result:
D:\temp>"c:\Program Files (x86)\MSBuild\12.0\Bin\MSBuild.exe" /nologo test.xml
Build started 2015/02/11 11:19:10 AM.
Project "D:\temp\test.xml" on node 1 (default targets).
Build:
Alice;Bob.not-config;Charlie
Done Building Project "D:\temp\test.xml" (default targets).

I needed to do something similar, the following worked for me.
<Target Name="Build">
<Message Text="#(Files->'%(Filename)'->Replace('.config', ''))" />
</Target>

Those functions works in properties only (as I know). So create target which will perform operation throw batching:
<Target Name="Build"
DependsOnTargets="ProcessFile" />
<Target Name="ProcessFile"
Outputs="%(Files.Identity)">
<PropertyGroup>
<OriginalFileName>%(Files.Filename)</OriginalFileName>
<ModifiedFileName>$(OriginalFileName.Replace(".config",""))</ModifiedFileName>
</PropertyGroup>
<Message Text="$(ModifiedFileName)" Importance="High"/>
</Target>
Do you really need in your example such kind of task? I mean there exists MSBuild Well-known Item Metadata
EDIT: I should specify that this task processes all items in #(Files).

i dont think you can use functions directly with itemgroups and metadata (that would be easy)
However you can use batching:
Taking the ideas from this post:
array-iteration
I was trying to trim an itemgroup to send to a commandline tool (i needed to lose .server off the filename)
<Target Name="ProcessFile" DependsOnTargets="FullPaths">
<ItemGroup>
<Environments Include="$(TemplateFolder)\$(Branch)\*.server.xml"/>
</ItemGroup>
<MSBuild Projects=".\Configure.msbuild"
Properties="CurrentXmlFile=%(Environments.Filename)"
Targets="Configure"/>
</Target>
<Target Name="Configure" DependsOnTargets="FullPaths">
<PropertyGroup>
<Trimmed>$(CurrentXmlFile.Replace('.server',''))</Trimmed>
</PropertyGroup>
<Message Text="Trimmed: $(Trimmed)"/>
<Exec Command="ConfigCmd $(Trimmed)"/>
</Target>

For MSBuild 12.0, here's an alternative.
<Target Name="Build">
<Message Text="$([System.String]::Copy("%(Files.Filename)").Replace(".config",""))" />
</Target>

Got the same problem (except with MakeRelative), so I passed with another solution : Using good old CreateItem that take a string and transform to Item :)
<ItemGroup>
<_ToUploadFTP Include="$(PublishDir)**\*.*"></_ToUploadFTP>
</ItemGroup>
<CreateItem Include="$([MSBuild]::MakeRelative('c:\$(PublishDir)','c:\%(relativedir)%(filename)%(_ToUploadFTP.extension)'))">
<Output ItemName="_ToUploadFTPRelative" TaskParameter="Include"/>
</CreateItem>
<FtpUpload Username="$(UserName)"
Password="$(UserPassword)"
RemoteUri="$(FtpHost)"
LocalFiles="#(_ToUploadFTP)"
RemoteFiles="#(_ToUploadFTPRelative->'$(FtpSitePath)/%(relativedir)%(filename)%(extension)')"
UsePassive="$(FtpPassiveMode)" ></FtpUpload>

Related

Calling MSbuild Publish

Is it possible to call MSbuild publish during build or in pre-buildevent or in post-buildevent? I'm trying to publish two of the web projects from a solution. I'm using file system publishing.Requirement here is , building solution should take care of publish those two web projects. Can any one please suggest ?
I wouldn't put too much deploy logic in a post-build event. It becomes "fragile".
Create a separate .msbuild file, and do the "extra" logic in it, instead of messing too much with the .csproj file.
Below is a basic example.
Place the xml below in an file call "MyBuildAndDeploy.msbuild", put it in the same folder as your .sln (or .csproj) file, and then use
msbuild.exe "MyBuildAndDeploy.msbuild" from the command line.
So below is a basic example of building the primary solution and then copying the files somewhere.
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="AllTargetsWrapper">
<PropertyGroup>
<!-- Always declare some kind of "base directory" and then work off of that in the majority of cases -->
<WorkingCheckout>.</WorkingCheckout>
<BuildResultsRootFolder>$(WorkingCheckout)\..\BuildResults</BuildResultsRootFolder>
</PropertyGroup>
<Target Name="AllTargetsWrapper">
<CallTarget Targets="BuildSolution" />
<CallTarget Targets="CopyBuildOutputFiles" />
</Target>
<Target Name="BuildSolution">
<MSBuild Projects="$(WorkingCheckout)\MySuperCoolSolution.sln" Targets="Build" Properties="Configuration=$(Configuration)">
<Output TaskParameter="TargetOutputs" ItemName="TargetOutputsItemName"/>
</MSBuild>
<Message Text="BuildSolution completed" />
</Target>
<Target Name="CopyBuildOutputFiles">
<MakeDir Directories="$(BuildResultsRootFolder)\$(Configuration)" Condition="!Exists('$(BuildResultsRootFolder)\$(Configuration)\')"/>
<ItemGroup>
<BuildOutputFilesExcludeFiles Include="$(WorkingCheckout)\**\*.doesnotexist" />
<BuildOutputFilesIncludeFiles Include="$(WorkingCheckout)\**\*.dll" Exclude="#(BuildOutputFilesExcludeFiles)" />
<BuildOutputFilesIncludeFiles Include="$(WorkingCheckout)\**\*.exe" Exclude="#(BuildOutputFilesExcludeFiles)" />
<BuildOutputFilesIncludeFiles Include="$(WorkingCheckout)\**\*.config" Exclude="#(BuildOutputFilesExcludeFiles)" />
<BuildOutputFilesIncludeFiles Include="$(WorkingCheckout)\**\*.pdb" Exclude="#(BuildOutputFilesExcludeFiles)" />
</ItemGroup>
<Copy SourceFiles="#(BuildOutputFilesIncludeFiles)"
DestinationFolder="$(BuildResultsRootFolder)\$(Configuration)\"/>
</Target>
</Project>

Additional paths in msbuild script

How to specify additional assembly reference paths for the MSBuild tasks?
I have following script so far, but can't figure out how to specify additional search paths.
<ItemGroup>
<ProjectsToBuild Include="..\Main\Main.sln" />
</ItemGroup>
<!-- The follwing paths should be added to reference search paths for the build tasks -->
<ItemGroup>
<MyAddRefPath Include="$(MSBuildProjectDirectory)\..\..\Build\Lib1" />
<MyAddRefPath Include="$(MSBuildProjectDirectory)\..\..\Build\Lib2" />
</ItemGroup>
<MSBuild
Projects="#(ProjectsToBuild)"
Properties="Configuration=Debug;OutputPath=$(BuildOutputPath)">
</MSBuild>
UPDATE:
Please show one complete working script which invokes original project, such as an SLN with multiple additional reference paths.
No suggestions on how to improve the project structure please.
I know how to build a good structure, but now it's the task of building an existing piece of crap.
I have finaly figured out how to do it:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectsToBuild Include="ConsoleApplication1\ConsoleApplication1.csproj" />
</ItemGroup>
<ItemGroup>
<AdditionalReferencePaths Include="..\Build\ClassLibrary1" />
<AdditionalReferencePaths Include="..\Build\ClassLibrary2" />
</ItemGroup>
<PropertyGroup>
<BuildOutputPath>..\Build\ConsoleApplication1</BuildOutputPath>
</PropertyGroup>
<Target Name="MainBuild">
<PropertyGroup>
<AdditionalReferencePathsProp>#(AdditionalReferencePaths)</AdditionalReferencePathsProp>
</PropertyGroup>
<MSBuild
Projects="ConsoleApplication1\ConsoleApplication1.csproj"
Properties="ReferencePath=$(AdditionalReferencePathsProp);OutputPath=$(BuildOutputPath)"
>
</MSBuild>
</Target>
The property you want to modify is AssemblySearchPaths. See the ResolveAssemblyReference task more information.
<Target Name="AddToSearchPaths">
<CreateProperty Value="x:\path\to\assemblies;$(AssemblySearchPaths)">
<Output PropertyName="AssemblySearchPaths" TaskParameter="Value" />
</CreateProperty>
</Target>
Making use of item groups, as in your example, it would look like:
<Target Name="AddToSearchPaths">
<CreateProperty Value="#(MyAddRefPath);$(AssemblySearchPaths)">
<Output PropertyName="AssemblySearchPaths" TaskParameter="Value" />
</CreateProperty>
</Target>
Looking in %WINDIR%\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets, you can see that the ResolveAssemblyReference Task is executed as part of the ResolveAssemblyReferences target. Thus, you want the newly added target to modify the AssemblySearchPaths property before ResolveAssemblyReferences is executed.
You've stated that you want to be able to modify the assembly search paths without modifying the project files directly. In order to accomplish that requirement you need to set an environment variable that will override the AssemblySearchPaths. With this technique you will need to provide every assembly reference path used by all the projects in the solutions. (Modifying the projects or copies of the projects would be easier. See final comments.)
One technique is to create a batch file that runs your script at sets the environment variable:
set AssemblySearchPaths="C:\Tacos;C:\Burritos;C:\Chalupas"
msbuild whatever.msbuild
Another way is to define a PropertyGroup in your custom msbuild file (otherwise known as the "hook" needed to make this work):
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectsToBuild Include="..\Main\Main.sln" />
</ItemGroup>
<PropertyGroup>
<AssemblySearchPaths>$(MSBuildProjectDirectory)\..\..\Build\Lib1;$(MSBuildProjectDirectory)\..\..\Build\Lib2</AssemblySearchPaths>
</PropertyGroup>
<Target Name="Build">
<MSBuild Projects="#(ProjectsToBuild)" Properties="AssemblySearchPaths=$(AssemblySearchPaths);Configuration=Debug;OutputPath=$(OutputPath)" />
</Target>
</Project>
Now if it were me, and for whatever unexplained reason I couldn't modify the project files to include the updated references that I am going to build with, I would make copies of the project files, load them into the IDE, and correct the references in my copies. Synching the projects becomes a simple diff/merge operation which is automatic with modern tools like mercurial (heck I'm sure clearcase could manage it too).
...and remember that you don't need to use a target for this, you can use project-scoped properties or items, as...
<ItemGroup>
<MyAddRefPath Include="$(MSBuildProjectDirectory)\..\..\Build\Lib1" />
<MyAddRefPath Include="$(MSBuildProjectDirectory)\..\..\Build\Lib2" />
</ItemGroup>
<PropertyGroup>
<MyAddRefPath>$(MSBuildProjectDirectory)\..\..\Build\Lib3</MyAddRefPath>
<!-- add in the property path -->
<AssemblySearchPaths>$(MyAddRefPath);$(AssemblySearchPaths)</AssemblySearchPaths>
<!-- add in the item paths -->
<AssemblySearchPaths>#(MyAddRefPath);$(AssemblySearchPaths)</AssemblySearchPaths>
</PropertyGroup>
...and if you do need to do this in a target to pick up paths from a dynamically populated item group, use inline properties, not the CreateProperty task (if you are not stuck in v2.0)
<Target Name="AddToSearchPaths">
<PropertyGroup>
<!-- add in the item paths -->
<AssemblySearchPaths>#(MyDynamicAddRefPath);$(AssemblySearchPaths)</AssemblySearchPaths>
</PropertyGroup>
</Target>

How do I execute tasks in MSBUILD?

I'm trying to wrap my head around MSBuild.
I have a very simple script that does the following so far:
Builds a solution and places it in my drop location.
I have created a <Target> and in it I would like to copy files and from my source control location and drop them in the drop location as well.
Eventually the script will have to create the folders etc. For now I am just trying to copy one file over to see how this works.
The solution builds and is placed in the drop location but no files are copied. The build log makes no mention of this Target ever being run.
What am I missing?
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<Target Name="Build">
<Message Text="Building msbuildintro" />
</Target>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<ProjectExtensions>
<ProjectFileVersion>2</ProjectFileVersion>
<Description></Description>
<BuildMachine>hw-tfs-build02</BuildMachine>
</ProjectExtensions>
<PropertyGroup>
/* Properties here*/
</PropertyGroup>
<ItemGroup>
<SolutionToBuild Include="$(BuildProjectFolderPath)/HostASPX/mySolution.sln">
<Targets></Targets>
<Properties></Properties>
</SolutionToBuild>
<CommonFiles Include="$(SolutionRoot)\trunk\folder\Common\Shared\js\somefile.js"></CommonFiles>
</ItemGroup>
<ItemGroup>
<ConfigurationToBuild Include="Release|Any CPU">
<FlavorToBuild>Release</FlavorToBuild>
<PlatformToBuild>Any CPU</PlatformToBuild>
</ConfigurationToBuild>
</ItemGroup>
<Target Name="CopyCommonData">
<Message Text="Copy Common Data" />
<Copy SourceFiles="#(CommonFiles)"
DestinationFiles="$(DropLocation)\Common\somefile.js" />
</Target>
</Project>
Thanks!
OH I get it.. Target Names are not 'made up'. They must be a specific Target Name found here:
http://msdn.microsoft.com/en-us/library/aa337604.aspx

Can I batch based on a Property (not just Items)?

I have a property group, like so:
<PropertyGroup>
<Platform>Win32;x64</Platform>
</PropertyGroup>
And I want to batch in an Exec task, like so:
<Exec Command='devenv MySolution.sln /Build "Release|%(Platform)"' />
But of course, as written I get an error:
error MSB4095: The item metadata %(Platform) is being referenced without an item name. Specify the item name by using %(itemname.Platform).
Can I batch tasks on properties that are lists? I suppose I could hack it by creating a placeholder ItemGroup with metadata and batch on that.
Since your property is separated by a ; you can directly create an item from it and then batch from that. For example.
<Project ToolsVersion="3.5" DefaultTargets="Demo" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Platform>Win32;x64</Platform>
</PropertyGroup>
<Target Name="Demo">
<ItemGroup>
<_PlatFormItem Include="$(Platform)"/>
</ItemGroup>
<Message Text="Platform: $(Platform)"/>
<Message Text="_PlatFormItem: #(_PlatFormItem)"/>
<Message Text="Platform.Identity: %(_PlatFormItem.Identity)"/>
<Exec Command='devenv MySolution.sln /Build "Release|%(_PlatFormItem.Identity)"' />
</Target>
</Project>
Here I'm batching using %(_PlatformItem.Identity) because Identity has the values (Win32 and x64).

MSBuild Copy task not copying files the first time round

I created a build.proj file which consists of a task to copy files that will be generated after the build is complete. The problem is that these files are not copied the first time round and I have to run msbuild again on the build.proj so that the files can be copied. Please can anyone tell me whats wrong with the following build.proj file:
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
<SourcePath Condition="'$(SourcePath)' == ''">$(MSBuildProjectDirectory)</SourcePath>
<BuildDir>$(SourcePath)\build</BuildDir>
</PropertyGroup>
<ItemGroup>
<Projects
Include="$(SourcePath)\src\myApp\application.csproj">
</Projects>
</ItemGroup>
<Target Name="Build">
<Message text = "Building project" />
<MSBuild
Projects="#(Projects)"
Properties="Configuration=$(Configuration)" />
</Target>
<ItemGroup>
<OutputFiles Include ="$(MSBuildProjectDirectory)\**\**\bin\Debug\*.*"/>
</ItemGroup>
<Target Name="CopyToBuildFolder">
<Message text = "Copying build items" />
<Copy SourceFiles="#(OutputFiles)" DestinationFolder="$(BuildDir)"/>
</Target>
<Target Name="All"
DependsOnTargets="Build; CopyToBuildFolder"/>
</Project>
The itemgroups are evaluated when the script is parsed. At that time your files aren't there yet. To be able to find the files you'll have to fill the itemgroup from within a target.
<!-- SQL Scripts which are needed for deployment -->
<Target Name="BeforeCopySqlScripts">
<CreateItem Include="$(SolutionRoot)\04\**\Databases\**\*.sql">
<Output ItemName="CopySqlScript" TaskParameter="Include"/>
</CreateItem>
</Target>
This example creates the ItemGroup named "CopySqlScript" using the expression in the Include attribute.
Edit:
Now I can read your script: add the CreateItem tag within your CopyToBuildFolder target