06 September 2006

How To: Get MSBuild to run a complete Target for each Item in an ItemGroup

Scenario: I am trying to create a generic target that will create my MSI packages.  This process consists of a number of distinct steps including MESSAGE tasks for logging and a call to either InstallShield or DevEnv.exe to build the actual MSI.

Problem:  My Team Builds are configured to create anywhere from zero to six MSIs during a build run.  I needed to configure the project so that I could list a number setup projects to be run and the build would create the MSIs from that list.  I also needed to be able to add additional metadata related to the MSI to be built.  In pseudocode, my task was this:

For each project in InstallerList
  Remove read-only flag from Installer project file 
  Set an Environment variable pointing to BinariesRoot 
  If PackagerType = "IS" Then Run InstallShield 
If PackagerType = "VS" Then Run DevEnv.exe Next project

 [The color coding on the pseudocode lines above will be used throughout the rest of this article to correlate the output received to the tasks needed.] 

So I wrote a test project file to see if I could get this to work.  My initial file looked something (actually exactly) like this:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Test" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <ItemGroup>
    <Package Include="CommonWebSetup.ism">
      <PackagerType>IS</PackagerType>
      <SetupProjFolder>CommonWebSetup</SetupProjFolder>
      <ISProductConfig>Server</ISProductConfig>
      <ISReleaseConfig>Release</ISReleaseConfig>
    </Package>
    <Package Include="CommonClientSetup.vdproj">
      <PackagerType>VS</PackagerType>
      <SetupProjFolder>CommonClientSetup</SetupProjFolder>
      <ISProductConfig>Client</ISProductConfig>
      <ISReleaseConfig>Release</ISReleaseConfig>
    </Package>
  </ItemGroup>


<Target Name="Test" >
	<Message Text="Removing read-only flag for %(Package.Identity)" Importance="High" />  

	<Message Text="Setting Environment variable for %(Package.Identity)" Importance="High" />  

	<Message Condition=" '%(Package.PackagerType)' == 'IS' " 
	         Text="Running InstallShield for %(Package.Identity)" Importance="High" />  

	<Message Condition=" '%(Package.PackagerType)' == 'VS' " 
	         Text="Running DevEnv.exe for %(Package.Identity)" Importance="High" />  

</Target>

</Project>

 I thought that this would do exactly what I wanted, since I had grouped all of the metadata into a nice self-containedItems.  So I ran it through MSBuild and received the following results:

 

__________________________________________________
Project "C:\test7.proj" (default targets):

Target Test:
Removing read-only flag for CommonWebSetup.ism
Removing read-only flag for CommonClientSetup.vdproj
Setting Environment variable for CommonWebSetup.ism
Setting Environment variable for CommonClientSetup.vdproj
Running InstallShield for CommonWebSetup.ism
Running DevEnv.exe for CommonClientSetup.vdproj

Build succeeded.
0 Warning(s)
0 Error(s)

Time Elapsed 00:00:00.03

 I've colored each MESSAGE task's output to show the results a little clearer. 

As you can see, the Test target was run only once with all of the items passed into it.  So Batching did not occur at the Target level, but rather at the Task level.  During this build, each Task had the opportunity to process  the batched metadata before passing control to the next Task in the Target.  This isn't what I expected, so I did a little research and (of course) this is by design.  I did a little more research and came across an MSBuild forum post that discussed a situation similar to mine.  In this post, Neil Enns (Lead Program Manager, Microsoft Visual Studio) went to great lengths to not only show how this could be done, but to explain the concepts behind the behavior was seen.  Kudos to Neil for making this whole "batching thing" just a little bit clearer (and giving me a quick answer so I can move on with my project).

Solution: To paraphrase the thread, you need to add an Outputs attribute to the Target.  This attribute must point to a metadata entry that is unique for each Item in the ItemGroup.  In my example above, the Identity (shown in the Include attribute of the Package element) is unique, so all I have to do is add it to the Outputs attribute of my Target and it will work.  Here' the updated project file (changes in bold) and MSBuild output:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Test" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <ItemGroup>
    <Package Include="CommonWebSetup.ism">
      <PackagerType>IS</PackagerType>
      <SetupProjFolder>CommonWebSetup</SetupProjFolder>
      <ISProductConfig>Server</ISProductConfig>
      <ISReleaseConfig>Release</ISReleaseConfig>
    </Package>
    <Package Include="CommonClientSetup.vdproj">
      <PackagerType>VS</PackagerType>
      <SetupProjFolder>CommonClientSetup</SetupProjFolder>
      <ISProductConfig>Client</ISProductConfig>
      <ISReleaseConfig>Release</ISReleaseConfig>
    </Package>
  </ItemGroup>


<Target Name="Test" Outputs="%(Package.Identity)" >
	<Message Text="Removing read-only flag for %(Package.Identity)" Importance="High" />  

	<Message Text="Setting Environment variable for %(Package.Identity)" Importance="High" />  

	<Message Condition=" '%(Package.PackagerType)' == 'IS' " 
	         Text="Running InstallShield for %(Package.Identity)" Importance="High" />  

	<Message Condition=" '%(Package.PackagerType)' == 'VS' " 
	         Text="Running DevEnv.exe for %(Package.Identity)" Importance="High" />  
</Target>

__________________________________________________
Project "C:\test7.proj" (default targets):

Target Test:
Removing read-only flag for CommonWebSetup.ism Setting Environment variable for CommonWebSetup.ism Running InstallShield for CommonWebSetup.ism
Target Test:
Removing read-only flag for CommonClientSetup.vdproj
Setting Environment variable for CommonClientSetup.vdproj
Running DevEnv.exe for CommonClientSetup.vdproj

Build succeeded.
0 Warning(s)
0 Error(s)

Time Elapsed 00:00:00.03

 

For additional information on Batching and Target Batching specifically, check out MSDN Help's MSBuild Concepts MSBuild Batching.

0 comments:

Post a Comment