- | rssFeed | My book on MSBuild and Team Build | Archives and Categories Saturday, June 01, 2013

Hijacking the Visual Studio Build Process

Have you ever wanted to use Visual Studio to manage project artifacts but wanted to have a fully custom build process? The recommend way to do this is to build a Custom Visual Studio Project System, but there is a much easier way for lightweight needs. In this post I’ll show you how to take an existing project and “replace” the build process used? For example, wouldn’t it be cool if you could develop a Chrome Extension with VS? When you do a build it would be great to generate the .zip file to for the Chrome Gallery in the output folder. Doing this is way easier than you might think.

Before I go over the steps to do this let me explain the “contract” between Visual Studio and MSBuild. The primary interactions around build in Visual Studio include the following actions in VS. Including what VS does when the action is invoked.

For more details you can read Visual Studio Integration (MSBuild). The easiest way to completely customize the VS build process is to do the following:

  1. Create the correct project based on the artifacts you plan on using (i.e. if you’re going to be editing .js files then create a web project)
  2. Edit the project file to not import any of the .targets files
  3. Define the following targets; Build, Rebuild and Clean

After that when you invoke the actions inside of VS the correct action will be executed in your project.

Let’s look at a concrete example. I’ve been working on a Chrome extension with Mads Kristensen the past few days. The entire project is available on github at https://github.com/ligershark/BestPracticesChromeExtension. When it came time to try out the extension or to publish it to the Chrome store we’d have to manually create it, which was annoying. We were using a standard web project to begin with. Here is how we made the workflow simple. Edited the project file to disable the Import statements. See below.

<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" 
    Condition="false" />
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" 
    Condition="false and '$(VSToolsPath)' != ''" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" 
    Condition="false" />

For each of these targets I added/updated the condition to ensure that the .targets file would never be loaded. The reason why I did not simply remove these is because in some cases VS may get confused and try to “upgrade” the project and re-insert the Import statements.

After that I added the following statements to the .csproj file.

  <PropertyGroup>
    <LigerShareChromeTargetsPath>$(BuildFolder)\ligersharek.chrome.targets</LigerShareChromeTargetsPath>
  </PropertyGroup>
  <Import Project="$(LigerShareChromeTargetsPath)" Condition="Exists($(LigerShareChromeTargetsPath))" />

Here I defined a new property to point to a custom .targets file and a corresponding Import statement. Now let’s take a look at the ligershark.chrome.targets file in its entirety.

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>
  </PropertyGroup>
  <ItemDefinitionGroup>
    <AppFileNameItem>
      <Visible>false</Visible>
    </AppFileNameItem>
    <AppFolderItem>
      <Visible>false</Visible>
    </AppFolderItem>
  </ItemDefinitionGroup>
  
  <UsingTask AssemblyFile="$(BuildLib)MSBuild.ExtensionPack.dll" TaskName="MSBuild.ExtensionPack.Compression.Zip" />
  
  <Target Name="Build">
    <MakeDir Directories="$(OutputPath)" />
    
    <ItemGroup>
      <_AppCandidateFilesToZip Remove="@(_AppCandidateFilesToZip)" />
      <_AppCandidateFilesToZip Include="@(Content);@(None)" />
    </ItemGroup>
    
    <FindUnderPath Path="$(AppFolder)" Files="@(_AppCandidateFilesToZip)">
      <Output TaskParameter="InPath" ItemName="_AppFilesToZip" />
    </FindUnderPath>
    <Message Text="App files to zip: @(_AppFilesToZip->'%(RelativeDir)%(Filename)%(Extension)')" />

    <PropertyGroup>
      <_AppFolderFullPath>%(AppFolderItem.FullPath)</_AppFolderFullPath>
    </PropertyGroup>

    <Message Text="Creating package .zip at [%(AppFileNameItem.FullPath)]" Importance="high" />
    <MSBuild.ExtensionPack.Compression.Zip 
      TaskAction="Create" 
      CompressFiles="@(_AppFilesToZip)" 
      ZipFileName="%(AppFileNameItem.FullPath)" 
      RemoveRoot="$(_AppFolderFullPath)" 
      CompressionLevel="BestCompression" />
  </Target>
  
  <PropertyGroup>
    <RebuildDependsOn>
      Clean;
      Build;
    </RebuildDependsOn>
  </PropertyGroup>
  <Target Name="Rebuild" DependsOnTargets="$(RebuildDependsOn)" />
  <Target Name="Clean">
    <!-- delete all the files in the output folder -->
    <ItemGroup>
      <_FilesToDelete Remove="@(_FilesToDelete)" />
      <_FilesToDelete Include="$(OutputPath)**\*" />
    </ItemGroup>
    <Message Text="Deleting files: @(_FilesToDelete)" />
    <Delete Files="@(_FilesToDelete)" />
  </Target>
</Project>

As you can see I defined the following targets; Build, Rebuild, and Clean. This is what we discussed earlier as the requirements. Now when I click the Build button in VS a .zip file containing the Chrome Extension is created in the bin\ folder. Additionally no other step (i.e. any typical build step) is performed here. The only thing happening is what is defined in the Build target here. Similarly when I Rebuild or Clean the Rebuild or Clean target is invoked respectively. This is  a pretty neat way to modify an existing project to completely replace the build process to suit your needs.

There are a few things in the .targets file that I’d like to point out.

UseHostCompilerIfAvailable

In the .targets file I have set the property declaration <UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>. When you are working with a project in Visual Studio there is a compiler, the host compiler, that Visual Studio has which can be used. For performance reasons in most cases this compiler is used. In this case since we do not want any standard build actions to be executed this performance trick is undesired. You can easily disable this by setting this property to false.

After this MSBuild will always be invoked when performing any build related action.

Visible metadata for Item Lists

When using MSBuild you typically represent files as values within an item list. One issue that you may encounter when modifying a project which will be loaded in VS is that the files inside of those item lists will show up in Visual Studio. If you want to disable this for a particular value in an item list you can add the well known metadata <Visible>false</Visible>. For example you could have the following.

<ItemGroup>
    <IntermediateFile Include="cache.temp">
        <Visible>false</Visible>
    </IntermediateFile>
</ItemGroup>

If you don’t want to add this to every value in the item list then you can set the default value using an ItemDefinitionGroup.  For example take a look at the elements below.

<ItemDefinitionGroup>
  <AppFileNameItem>
    <Visible>false</Visible>
  </AppFileNameItem>
  <AppFolderItem>
    <Visible>false</Visible>
  </AppFolderItem>
</ItemDefinitionGroup>

After this declaration is processed if the Visible metadata is accessed for AppFileNameItem or AppFolderItem on a value which does not declare Visible, then false is returned.

 

The rest of the content of the .targets file is pretty straightforward. This post shows a basic example of how you can take an existing Visual Studio project and completely replace the build process.

 

This project is open source at https://github.com/ligershark/BestPracticesChromeExtension.

 

Sayed Ibrahim Hashimi | http://msbuildbook.com | @SayedIHashimi

msbuild | MSBuild 4.0 | Visual Studio | Visual Studio 2012 Saturday, June 01, 2013 6:47:16 PM (GMT Daylight Time, UTC+01:00)  #     |