MSBuild using %(RecursiveDir) as part of file name - msbuild

As part of our build script we copy user messages files from a subdirectory and want to append the name of the subdirectory to the message files.
i.e. msg\0\message.std > msg\message0.std
I have tried using
<Copy SourceFiles="#(MessageFiles)"
DestinationFiles="#(MessageFiles->'$(BuildRoot)\%(Filename)%(RecursiveDir)%(Extension)'"/>
However this tries to copy the file to ..\message0.std.
Is there anyway to either suppress the trailing '\' from %(RecursiveDir) or make up the destination name another way?

You can do something like the following:
<Target Name="DoIt">
<ItemGroup>
<MessageFiles2 Include="#(MessageFiles)">
<SubDir>$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName(%(MessageFiles.RecursiveDir)))))</SubDir>
</MessageFiles2>
</ItemGroup>
<Message Text="#(MessageFiles2->'$(BuildRoot)\%(Filename)%(SubDir)%(Extension)')"/>
</Target>
or if you wish to blow up the mind of the person who'll try to maintain your work:
<Target Name="DoIt">
<Message Text="#(MessageFiles->'$(BuildRoot)\%(Filename)$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName($([System.String]::Copy('%(MessageFiles.RecursiveDir)'))))))%(Extension)')"/>
</Target>
This is not a complete solution, but you can start from here. Also note that both above examples will break if RecursiveDir is empty i.e. your message file lies directly at the root folder.
You can find more information in MSBuild Property Functions blog post.
As an another way to handle your problem you can always create Custom Task.

Related

Build script to copy files from various source folder to various destination folder

The basic requirement is to copy the various files and folders from different solution/project directories to the single build_output folder(/subfolders).
Currently, I am doing this operation using the Robocopy commands. The only issue is my script is too long just using multiple Robocopy commands.
<Copy SourceFiles="$(Docs)\Manual.pdf" DestinationFolder="$(BuildPath)\Help"/>
<RoboCopy Source="$(Web1)" Destination="$(BuildPath)" Files="*.aspx" Options="/E"/>
<RoboCopy Source="$(Web1)\Images" Destination="$(BuildPath)\Images" Files="*.jpg;*.png" Options="/E"/>
<RoboCopy Source="$(Web2)\Images" Destination="$(BuildPath)\Images" Files="*.jpg;*.png" Options="/E"/>
<!--- 100s of such RoboCopy & Copy commands (note that in last two commands i need to copy from different sources to same destination -->
How this job is implemented in real enterprise applications, so that
the build script is concise and clear.
Is my thinking below is the way to approach the solution. If yes, can anybody provide me sample steps using MSBuild or CommandScript easily. (free to use any MSBuild extensions)
Define the mapping of the all source folders, file types (can be xyz.png/.png/.*) and the destination path.
Copy the files (Robocopy) using the above mentioned mappings using a single target or task)
Is there any other better way to do this problem?
Insights/Solution ???
I do exactly this sort of thing to stage build output for harvesting by the installer build. I have a custom targets file for consistent processing and have some msbuild property files with the item groups describing that needs to be done.
<ItemGroup Label="AcmeComponent1Payload">
<FileToHarvest Include="$(SourceRoot)AcmeProjects\ServerManager\$(Configuration)\**\*;
$(SourceRoot)Library\SQLServerCompact\**\*;
$(SourceRoot)Utility Projects\PropertyDataValidator\PropertyDataValidator\bin\$(Configuration)\PropertyDataValidator.*"
Exclude="$(SourceRoot)Server Manager Projects\AcmeServerManager\$(Configuration)\IntegrationTests.*;
$(SourceRoot)Server Manager Projects\AcmeServerManager\$(Configuration)\**\Microsoft.Practices.*.xml;
$(SourceRoot)Server Manager Projects\AcmeServerManager\$(Configuration)\obj\**\*;
$(SourceRoot)Server Manager Projects\AcmeServerManager\$(Configuration)\**\Microsoft.VisualStudio.*;
$(SourceRoot)Server Manager Projects\AcmeServerManager\$(Configuration)\**\Microsoft.Web.*;
$(SourceRoot)Utility Projects\PropertyDataValidator\PropertyDataValidator\bin\$(Configuration)\PropertyDataValidator.xml">
<Group>AcmeServerManager</Group>
<SubDir>Utilities\</SubDir>
</FileToHarvest>
</ItemGroup>
The custom targets file has the functionality to process it.
<Target Name="CopyFiles">
<Copy Condition="#(FileToHarvest)!=''"
SourceFiles="#(FileToHarvest)"
DestinationFiles="#(FileToHarvest->'$(OutputPath)\%(Group)\%(SubDir)%(RecursiveDir)%(Filename)%(Extension)')"
OverwriteReadOnlyFiles="true"
SkipUnchangedFiles="true" />
</Target>
You can make the properties file as simple or as complicated as you like. I use multiple ones and import them into the project file using wildcards.
Thanks #daughey, I got my first part working, where I need to copy from different sources to the same destination.
<!--Declare an ItemGroup that points to source Locations-->
<ItemGroup>
<ItemToCopy Include="$(Web1)\Audit"/>
<ItemToCopy Include="$(Utilities)\Service"/>
<ItemToCopy Include="$(Web1)\NET"/>
</ItemGroup>
<!--Declare an ItemGroup that points to destination Locations-->
<ItemGroup>
<DestLocations Include="$(BuildPath)" />
</ItemGroup>
<Target Name="CopyFiles">
<!-- Run the copy command to copy the item to your dest locations-->
<!-- The % sign says to use Batching. So Copy will be run for each unique source ItemToCopy(s) in the DestLocation.-->
<RemoveDir Directories="$(BuildPath)"/>
<Message Importance="high" Text="Deploy folder is $(BuildPath)"/>
<RoboCopy Source="%(ItemToCopy.FullPath)" Destination="$(BuildPath)" Files="*.dll"/>
</Target>
After struggling with Item Metadata for Task Batching, the second part is also working great.
Scenario: Copy files from list of source directories to output directory on their respective sub-folders
$(Web1)\Audit*.dll => $(BuildPath)\Audit*.dll
$(Utilities)\Service*.jpg => $(BuildPath)\Utilities\Service*.jpg
Solution
<!--Modify an ItemGroup with metadata-->
<ItemGroup>
<ItemToCopy Include="$(Web1)\Audit">
<ToPath>$(BuildPath)\Audit</ToPath>
<FileType>*.dll</FileType>
</ItemToCopy>
<ItemToCopy Include="$(Utilities)\Service">
<ToPath>$(BuildPath)\Utilities\Service</ToPath>
<FileType>*.jpg;*.bmp</FileType>
</ItemToCopy>
</ItemGroup>
<Target Name="CopyBatch">
<RoboCopy Source="%(ItemToCopy.Identity)" Destination="%(ItemToCopy.ToPath)" Files="%(ItemToCopy.Filetype)"/>
</Target>

Specify build directory name

I need project to be built into folder:
\bin\Debug 1.0.3.4
Where 1.0.3.4 is a current building assembly version specified in assembly: AssemblyVersion attribute.
I tried using different variables like $(AssemblyVersion), GetAssemblyIdentity task but had no luck.. I'm not so good at using MSBuild.
A quick and dirty solution to this is to, after the build, use GetAssemblyIdentity to extract the version info and then move the $(TargetFile) to the appropriate directory.
This assumes you don't have any dependencies going on. Otherwise, you'll need to modify the $(TargetPath), $(TargetName), and $(TargetExt) or go with the "proper" way of doing it - modifying $(OutputPath) dynamically.
<Target Name="AfterBuild" AfterTargets="Build">
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="AssemblyIdentity"/>
</GetAssemblyIdentity>
<PropertyGroup>
<Version>#(AssemblyIdentity->'%(Version)')</Version>
</PropertyGroup>
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(OutDir)$(Version)" />
</Target>
The proper way would be to modify $(OutDir) aka $(OutputPath), but that would involve things like modifying the AssemblyVersion.cs file (or more complexly, a copy of it injected automatically into the build chain).
You could also instead parse the version from the AssemblyVersion.cs file.

MSBuild: Ignore targets that don't exist

Solution1.sln contains two projects:
ProjectA.csproj
ProjectB.csproj
ProjectB has a custom target called "Foo". I want to run:
msbuild Solution1.sln /t:Foo
This will fail because ProjectA doesn't define the "Foo" target.
Is there a way to make the solution ignore the missing target? (E.g., do nothing if the target doesn't exist for a specific project) without modifying the SLN or project files?
There is a two-part solution if you don't want to edit the solution or project files and you're happy for it to work from MSBuild command-line but not from Visual Studio.
Firstly, the error you get when you run:
MSBuild Solution1.sln /t:Foo
Is not that ProjectA does not contain a Foo target but that the solution itself does not contain a Foo target. As #Jaykul suggests, setting the MSBuildEmitSolution environment variable will reveal the default targets contained within the solution metaproj.
Using the metaproj as inspiration you can introduce a new file "before.Solution1.sln.targets" next to the solution file (the file name pattern is important) with contents like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Foo">
<MSBuild Projects="#(ProjectReference)" Targets="Foo" BuildInParallel="True" Properties="CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)" SkipNonexistentProjects="%(ProjectReference.SkipNonexistentProjects)" />
</Target>
</Project>
The MSBuild element is mostly just copied from the solution metaproj's Publish target. Adjust the target name and any other details to suit your scenario.
With this file in place, you'll now get the error that ProjectA does not contain the Foo target. ProjectB may or may not build anyway depending on inter-project dependencies.
So, secondly, to solve this problem we need to give every project an empty Foo target which is then overridden in projects that actually already contain one.
We do this by introducing another file, eg "EmptyFoo.targets" (name not important) that looks like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Foo" />
</Project>
And then we get every project to automatically import this targets file either by running MSBuild with an extra property, eg:
MSBuild Solution1.sln /t:Foo /p:CustomBeforeMicrosoftCommonTargets=c:\full_path_to\EmptyFoo.targets
Or include the CustomerBeforeMicrosoftCommonTargets property in the Properties attribute on the MSBuild element in the first targets file where you could optionally specify the full path relative to the $(SolutionDir) property.
However, if you're willing to run Foo in conjunction with any of the default solution targets (ie Build, Rebuild, Clean, or Publish) you could take some inspiration for how the Web Publishing Pipeline in MSBuild uses the DeployOnBuild property to call the Publish target on Web projects in a solution containing other project types that don't support publishing.
More info on the before.Solution1.sln.targets file here:
http://sedodream.com/2010/10/22/MSBuildExtendingTheSolutionBuild.aspx
You can target those by project name, like /t:project:target (might need quotes, I can't remember).
You can find all the generated targets by setting the environment variable MSBuildEmitSolution = 1 ... which causes msbuild to save to disk the temp .metaproj file which it generates for your solution. That file has all those targets defined in it, just open it up and take a look ;)
Maybe not the best answer but a reasonable hack.
msbuild ProjectA.csproj
msbuild ProjectB.csproj /t:Foo
When msbuild building solution - msbuild emits only limited set of targets into it's .metaproj file, and afaik - you can't build custom target through building sln file, you have to use original project1.csproj or custom build script.
Just for reference:
Use ContinueOnError when using MSBuildTask or -p:ContinueOnError=ErrorAndContinue when using (dotnet) msbuild
It may be in limited scenarios helpful: For example you have a list of .csproj files and want attach metadata only to specific project file items then you could write something like this:
<Target Name="UniqueTargetName" Condition="'$(PackAsExecutable)' == 'Package' Or '$(PackAsExecutable)' == 'Publish'" Outputs="#(_Hello)">
<ItemGroup>
<_Hello Include="$(MSBuildProjectFullPath)" />
</ItemGroup>
</Target>
<Target Name="BuildEachTargetFramework" DependsOnTargets="_GetTargetFrameworksOutput;AssignProjectConfiguration;_SplitProjectReferencesByFileExistence"
Condition="$(ExecutableProjectFullPath) != ''">
<Message Text="[$(MSBuildThisFilename)] Target BuildEachTargetFramework %(_MSBuildProjectReferenceExistent.Identity)" Importance="high" />
<MSBuild
Projects="%(ProjectReferenceWithConfiguration.Identity)"
Targets="UniqueTargetName"
ContinueOnError="true">
<Output TaskParameter="TargetOutputs" ItemName="_Hallo2" />
</MSBuild>
<Message Text="[$(MSBuildThisFilename)] ########### HELLO %(_Hallo2.Identity)" Importance="high" />
</Target>

How do I move a bunch of files using a Move MSBuild task and a wildcard?

I have a folder with files that have names starting with App_Web_ and ending with .dll. I don't know what's in between those parts and I don't know the number of files. I need MSBuild to move those files into another folder.
So I composed this:
<Move
SourceFiles="c:\source\App_Web_*.dll"
DestinationFolder="c:\target"
/>
but when the target runs I get the following output:
error MSB3680: The source file "c:\source\App_Web_*.dll" does not exist.
The files are definitely there.
What am I doing wrong? How do I have the files moved?
You cannot use regular expression directly in task parameters. You need to create an item containing list of files to move and pass its content to the task:
<ItemGroup>
<FilesToMove Include="c:\source\App_Web_*.dll"/>
</ItemGroup>
MSBuild will expand regular expression before passing it to the task executor. So later in some target you may invoke Move task:
<Target Name="Build">
<Move
SourceFiles="#(FilesToMove)"
DestinationFolder="C:\target"
/>
</Target>

MSBuild, clear out multiple files in a project

I am getting the following error when trying to clear out files within my project
LC error LC0000: 'Could not find file 'E:\CI\BuildServer\RMS-Transition\Group\dev\Controls\Properties\licenses.licx'.'
My MSBuild task looks like this...
<Target Name="ClearLicenseFiles">
<ItemGroup>
<LicenseFiles Include="..\**\*.licx"/>
</ItemGroup>
<WriteLinesToFile File="%(LicenseFiles.FullPath)" Lines="" Overwrite="true"/>
</Target>
What is going on? It seems to find all of the .licx files just fine but when it goes to write to them, they don't exist... and according to the documentation the WriteLinesToFile task should create the file anyways if it doesn't already exist.
I am starting to believe that this is a bug with MSBuild... the license files are being DELETED not overwritten as you would expect. Someone else has had this issue as well (comment on bottom of this msdn article)
This is my solution... I created a file that is empty named empty.txt right next to my msbuild proj and then copied this file onto the licx files.
<Target Name="ClearLicenseFiles">
<ItemGroup>
<LicenseFiles Include="..\**\*.licx"/>
</ItemGroup>
<Copy SourceFiles="empty.txt" DestinationFiles="%(LicenseFiles.FullPath)"/>
</Target>