MSBuild: How to get all generated outputs
Earlier today a reader of my book emailed me the following question
I am trying to get a list of all the files being generated when MSBuild build a certain project (they should be all the files in .bin directory).
When I used the TargetOutputs in MSBuild task it gives me the main output only, which is the .exe or .dll. But any other files like dependency assemblies will not be listed in the TargetOutputs collection.
So it sounds like he is looking for a generic way of determining all the files that have been created (moved) during the build process. I’m assuming that he has a few projects and is using a build script to run the process, and then he wants to move the files to another location. If you have a few projects, then obviously the easiest way is to manually examine the output paths of each project and copy the file as necessary. This is not an ideal scenario, because if you add a project to your process you have to make sure to remember to perform the copy manually. Anywayz, this type of behavior would be useful in many different scenarios. Now let’s talk about how to achieve this.
I took another look at the Microsoft.Common.targets file and determined that there is not a straight forward method of performing this action. So, after thinking about this for a little while I think I have come up with a solution. It’s a little confusing, but actually quite simple.
The solution lies in what I’m calling “MSBuild Inheritance”, which I’ve blogged about previously in the entries at:
This is where you take an existing MSBuild file and without modifying it you add behavior and or data to it. Now let’s see how this idea can be used to add the desired behavior described above. For this sample I have created a sample Windows Forms app, as well as an external build script, buildWrapper.proj. This build file is shown below.
<Project DefaultTargets="GetOutputs" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Output ItemName="ProjectOutputs" TaskParameter="TargetOutputs"/>
<Message Text="%0a%0dProjectOutputs:%0a%0d @(ProjectOutputs,'%0a%0d ')" />
These elements will only be processed when in the context of the
above Target. This is because of the Condition constraint on these items
<Import Project="$(ProjectFile)" Condition="'$(ImportProjectFile)'=='true'"/>
Here we need to override the Build target with our own that will
generate the correct results.
<Output ItemName="AllOutputs" TaskParameter="Include"/>
<Message Text="Custom build invoked!" Importance="high"/>
What this does is twofold:
1. Defines the GetOutputs target which will drive the process
2. Extends the behavior of the normal build to create the proper outputs
If you take a look at the Microsoft.Common.targets you will see that the default definition of the Build target is defined as follows:
Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
The purpose of this target is to define what targets it depends on and to create the outputs, by itself it doesn’t actually perform any action. Because of this we can override it in our buildWrapper.proj file. So the bottom half of the buildWrapper.proj file will conditionally import the project file and then conditionally re-define the Build target. Inside the new Build target we simply examine the output directory and create an item that contains all files that lie underneath it. In the image below you will see the result of this, specifically take a look at the region highlighted in red.
Now we can see that we have successfully achieved our goals.
Every time I describe this process I feel like it is still un-clear. If you have questions please let me know.
Sayed Ibrahim Hashimi
You can download all the files at http://www.sedodream.com/content/binary/OutExample.zip