Jasinski Technical Wiki

Navigation

Home Page
Index
All Pages

Quick Search
»
Advanced Search »

Contributor Links

Create a new Page
Administration
File Management
Login/Logout
Your Profile

Other Wiki Sections

Software

PoweredBy

Using MSBuild Community Tasks to create an installer

RSS
Modified on Tue, Feb 03, 2009, 12:59 PM by Shaggy13spe Categorized as ASP·NET Web Forms, Code Sample, MSBuild, Visual Studio and Developer Tools, ·Net Framework
{outline||<1> - |Step <1> - } Contributed by: Michael Morrison

Background

Recently, I was tasked with building an installer for the project I'm currently on. The solution we had built consisted of a Windows application that hosted a web browser control, a web application that actually did all the work, several class libraries and two Windows services that both the Windows application and web application communicated with. Making an installer/setup project for each component wasn't an option. We needed one installer. One that would not only install the windows application, but also check to see if certain components like MSMQ and IIS are installed and configured properly, deploy the website to the machine, install the two Windows services and some other things.

Unfortunately, one msi cannot call another msi file (only one is allowed to run at any given time), so my solution was to create an msi installer project in VS for the Windows application and add a Custom Action to launch a console application that would take care of the rest. So I created a Solution folder called Setup which contained a Setup project and the Console application and also a Web Deployment project.

The Web Deployment project is an add-on that MS created that among other things (like being able to use 1 dll for the website instead of the dynamically generated "nameless" dlls a normal build creates) packages up a website project and allows you to "deploy" it. But this wasn't going to work for us, because I wanted it to build/package it up but not deploy it. It would be "deployed" by my console app. So how was I going to do this? If only there was a way I could zip up the files in the website, then I could include them as an embedded resource of the console app and have it extract it out to the machine upon installation. But running the Web Deployment project and then zipping up the files it creates and putting it into the console app's file structure/project manually was too cumbersome and wouldn't have passed muster. What to do, what to do?

Solution

This brings us to the point of this article. Well, knowing that VS uses MSBuild as its build engine, and having delved ever so slightly into MSBuild itself previously, I was convinced there must be some way of doing this. I looked at all the options available to MSBuild and there wasn't a zip facility like there is with NAnt. Well now what?

A quick search on the net for "Zip" and "MSBuild" brought me to a site that hosts an open-source project called MSBuild Community Tasks. One of the tasks is a Zip task that will Zip up files for you. Perfect! So how to do what I did? Well download the tasks and install them as normal. (These tasks will work on any project that contains an MSBuild proj file (any type except Website project)).

Editing the Deployment Project File

So, first I right clicked on my deployment project and chose to "Open Project File" (for other project types you have to "Unload Project" first, then choose to "Edit" it) . I changed my Web Deployment project to Output to a single folder, regardless of the build configuration:

  
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>false</DebugSymbols>
    <OutputPath>.\Web</OutputPath>
    <EnableUpdateable>false</EnableUpdateable>
    <UseMerge>true</UseMerge>
    <SingleAssemblyName>RCCMSWeb</SingleAssemblyName>
    <DeleteAppCodeCompiledFiles>true</DeleteAppCodeCompiledFiles>
    <DeleteAppDataFolder>true</DeleteAppDataFolder>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugSymbols>false</DebugSymbols>
    <OutputPath>.\Web</OutputPath>
    <EnableUpdateable>false</EnableUpdateable>
    <UseMerge>true</UseMerge>
    <SingleAssemblyName>RCCMSWeb</SingleAssemblyName>
    <DeleteAppCodeCompiledFiles>true</DeleteAppCodeCompiledFiles>
    <DeleteAppDataFolder>true</DeleteAppDataFolder>
  </PropertyGroup>
  

Adding the Reference to MSBuild Community Tasks

Then I added a reference to the MSBuild Community Tasks under the line for the WebDeployment tasks import

  <Import Project="$(MSBuildExtensionsPath)\Microsoft\WebDeployment\v9.0\Microsoft.WebDeployment.targets" />
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />


Essential Code

and then the meat of the operation:

  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.WebDeployment.targets.

  <Target Name="BeforeBuild">
    <Copy SourceFiles="$(SourceWebPhysicalPath)\PrintForms\**\*.*" DestinationFolder="$(OutputPath)" />
  </Target>
  <Target Name="BeforeMerge">
  </Target>
  <Target Name="AfterMerge">
  </Target>
  --> 
  <Target Name="AfterBuild">
    <ItemGroup>
      <ZipFiles Include="$(OutputPath)\**\*.*" Exclude="*.zip" />
    </ItemGroup>
    <Zip Files="@(ZipFiles)" WorkingDirectory="$(OutputPath)\" ZipFileName="..\RCCMSWeb.zip" ZipLevel="9" />
    <Attrib Files="..\WinComponentsSetup\zip\RCCMSWeb.zip" Normal="true" />
    <Delete Files="..\WinComponentsSetup\zip\RCCMSWeb.zip" Condition="Exists('..\WinComponentsSetup\zip\RCCMSWeb.zip')" />
    <Move SourceFiles="..\RCCMSWeb.zip" DestinationFolder="..\WinComponentsSetup\zip" />
    <Attrib Files="..\WinComponentsSetup\zip\RCCMSWeb.zip" ReadOnly="true" />
    <RemoveDir Directories="$(OutputPath)" />
  </Target>


Explaining the MSBuild XML

So first, I've added a Target called AfterBuild. MSBuild recognizes this as a default and knows it's going to do this after it's finished building the website. I create an ItemGroup and create a group called ZipFiles that sets up some parameters, namely to include every file in the output folder excluding any zip files. Then the Zip task, takes that item and uses it as it's Files property, sets the Working Directory (so paths will be relative to that directory) names the ZipFile and how many levels down it should go to zip.

Next we have to get that zip file into our console app project. If this is the first time we've built this, it doesn't exist, but assuming we've built the solution before, and that we're under source control, we need to delete the existing zip file in the destination(console app) directory. The Attrib task lets us set the file to Normal (not Read-Only). Then we delete the file (as the Delete task suggests). Then we move the newly built zip file and reset it's attribute to Read-Only.

Finally, since we no longer need it, we remove our OutputPath directory (the one created by the web deployment build). Now, if this is the first time we've done this, the only thing left to do is to pull the zip file into the console app's project (via Add...Existing Item...), set it's Build Action to Embedded Resource and check in the project file.

Voila!

ScrewTurn Wiki version 3.0.1.400. Some of the icons created by FamFamFam. Except where noted, all contents Copyright © 1999-2017, Patrick Jasinski.