Photo by: mistyeiz |
A couple of weeks ago I had one hell of a time with an MSBuild script. I felt like I was going crazy, I couldn’t figure out what was going on. I was following all of the examples on the net and I still couldn’t figure it out.
The Internet has tons of sites about how to capture a list of files to delete or copy. I’m used to working with NAnt so grabbing a list of files was pretty easy for use in the copy or delete. Moving to the MSBuild version was not that difficult. Until I introduced TeamBuild to the build process.
How to Capture a list of files in MSBuild
You can then use the Item like this:
<ItemGroup>
<TestFiles Include="C:\Binaries\Release\_PublishedWebsites\WebApplication123\**\*.*" />
</ItemGroup>
<Message Text="@(TestFiles)" />
This will print the list of files to the screen. You can also use this in the delete or copy task to work with them.
Caveat
Normally this works, except when you’re using TeamBuild and the $(OutDir) property. Aaron Halberg has posted about the issues with $(OutputPath) and $(OutDir) here.
In a nutshell $(OutDir) is overwritten by the Team Build targets. Please read those links for some more in depth background.
The Problem
When utilizing the $(OutDir) property within a list while utilizing Team Build we have to create the List with CreateItem in a target because of a known limitation with emitting properties.
The issue has to do with not being able to access items and properties that are created within a target until the target execution actually completes.
So, if you were to run CreateProperty or CreateItem and immediately execute the CallTarget task to invoke another target that needed access either an item or property that was just created, you will be out of luck. We don’t publish dynamic properties or items until the target that created them is done executing. This is a known issue for Whidbey. Fortunately the workaround is simple: have one target emit the items/properties and finish execution before you run the next target that uses them. You can sequence the execution of these two targets via a DependsOnTargets attribute on a master target – alternatively the master target can use CallTarget to invoke both the targets sequentially.
Author: Faisal Mohammood
So what does this mean? We have to create a Target that creates the list, then call that target prior to using the list.
The Solution
To understand this correctly, lets take a look a task that DOES NOT WORK, then we’ll look at one that does work:
Does Not Display Files
The example below will NOT display a list of output files.
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="DesktopBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v8.0\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v8.0\WebApplications\Microsoft.WebApplication.targets"/>
<ItemGroup>
<SolutionToBuild Include="$(SolutionRoot)\development\webapplication8\webApplication8.sln" />
</ItemGroup>
<ItemGroup>
<ConfigurationToBuild Include="Release|Any CPU">
<FlavorToBuild>Release</FlavorToBuild>
<PlatformToBuild>Any CPU</PlatformToBuild>
</ConfigurationToBuild>
</ItemGroup>
<PropertyGroup>
<PublishedOutputFolder>_PublishedWebsites\WebApplication8\</PublishedOutputFolder>
</PropertyGroup>
<PropertyGroup>
<TestFolder>$(OutDir)$(PublishedOutputFolder)</TestFolder>
</PropertyGroup>
<ItemGroup>
<TestFiles Include="$(OutDir)$(PublishedOutputFolder)**\*.*" />
<!– Uncomment the line below to see that it will actually work if you supply a full path –>
<!–<TestFiles Include="C:\Binaries\Release\_PublishedWebsites\WebApplication8\**\*.*" />–>
</ItemGroup>
<Target Name="AfterCompile">
<CallTarget Targets="PrintText" />
</Target>
<Target Name="PrintText">
<Message Text="test: $(OutDir)$(PublishedOutputFolder)" />
<Message Text="Test Files: @(TestFiles)" />
</Target>
</Project>
Displays Files
This example msbuild file (nearly identical to the one above – with a few minor changes) will display a list of files.
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="DesktopBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v8.0\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v8.0\WebApplications\Microsoft.WebApplication.targets"/>
<ItemGroup>
<SolutionToBuild Include="$(SolutionRoot)\development\webapplication8\webApplication8.sln" />
</ItemGroup>
<ItemGroup>
<ConfigurationToBuild Include="Release|Any CPU">
<FlavorToBuild>Release</FlavorToBuild>
<PlatformToBuild>Any CPU</PlatformToBuild>
</ConfigurationToBuild>
</ItemGroup>
<PropertyGroup>
<PublishedOutputFolder>_PublishedWebsites\WebApplication8\</PublishedOutputFolder>
</PropertyGroup>
<PropertyGroup>
<TestFolder>$(OutDir)$(PublishedOutputFolder)</TestFolder>
</PropertyGroup>
<Target Name="CreateProperties">
<CreateItem Include="$(OutDir)$(PublishedOutputFolder)**\*.*">
<Output TaskParameter="Include" ItemName="ActualFiles"/>
</CreateItem>
</Target>
<Target Name="AfterCompile">
<CallTarget Targets="CreateProperties" />
<CallTarget Targets="PrintText" />
</Target>
<Target Name="PrintText">
<Message Text="test: $(OutDir)$(PublishedOutputFolder)" />
<Message Text="Actual Files: @(ActualFiles)" />
</Target>
</Project>
Conclusion
If you’re using the $(OutDir) property with Team Build and you need to create a dynamic list of files, you will need to create the list through a target using CreateItem task. This also applies to MSBuild properties as well. For a full understanding of why you have to call a target to create the properties prior to use them, read these two links – MSBuild Blog – Sayed Ibrhim Hashimi.
Notes: If you’re using a normal MSBuild property such as $(OutputFolder) – and not using TeamBuild – you can use the following to get the list of files:
<ItemGroup>
<TestFiles Include="$(OutputFolder)**\*.*" />
</ItemGroup>
Leave a Reply
You must be logged in to post a comment.