In the process of automating my build process for a project in 2008 (and VB6), I needed to have several additional steps happen during the build.
If the steps were directly related to a specific project within the overall solution, I could just alter the VBPROJ or CSPROJ file directly to include the required targets, no big deal. But in this case, I needed these steps to happen AFTER the overall build (at least, the build of the normal .NET solution).
I’m also using TFS and its build system to (eventually) automate the entire build process from a standalone server. But I’d like for the build process to be executable from any old workstation (assuming the required prerequisite files and apps are installed) for debugging and troubleshooting purposes. Anybody that would say that automating the build process isn’t an entire development project in it’s own right is out of their mind!
Furthermore, I wanted to impact the overall project build process as little as possible, so I really didn’t want to go traipsing around in the TFSBuild.proj file, or altering any of the TFS config at all if possible.
I knew that VBPROJ and CSPROJ files are nothing more than MSBUILD xml config files, which can be easily extended. But I wanted/needed something that would execute AFTER all the projects in my solution were built (and possibly before, but the same principle applies).
Then it struck me. What about a dummy project? One that could participate in the build process just as every other project in the solution, but which didn’t ACTUALLY build any specific .net assembly at all.
First step, add a new project. I picked an “empty VB project” for this.
But when I did this, I got an error
Rats, VS was insisting that the project have a sub Main, and I don’t really want anything compiling in this project. But a little poking around in the VBPROJ file turned up this IMPORTS line
<Import Project="$(MSBuildBinPath)\Microsoft.VisualBasic.targets" />
Remove it, and the Sub Main error goes away. Those VisualBasic.targets are what controls that validation.
If you try and build the project at this point, though, you’ll get an error that no “Build” target exists in the file. That’s because the default Build target is defined by that VisualBasic.Targets line, and we just removed it.
So, I added my own Build target near the end of the file, with some testing tasks in it just to see what happens.
<Target Name="Build">
<Message Text="The Test build was called" />
<Exec Command="Pause" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
Interestingly, the Message text does NOT display in the VS output window, but the EXEC task does (and the build is NOT paused by the PAUSE command).
Also, I’ve left them in for reference above, but when you remove the import of the Microsoft.VisualBasic.Targets file, the BeforeBuild and AfterBuild targets are effectively moot. They won’t do anything anymore, so you can remove them.
Editing the Project in the IDE
One problem that you’ll run into immediately with all this is that the Visual Studio IDE wants to handle those VBPROJ files special. And if you have the VBPROJ file opened in the IDE, the ONLY way you can change it is via the VS GUI. But the VS GUI doesn’t give you access to all the nooks and crannies of a VBPROJ file that you need to mess with for this to work.
One way to get around this problem is to unload the project (Right click on the Project in the Solution Explorer and select unload. Then Right click on the project again and you should see a “Edit your project name” menu item. Select it and your project should load up in VS just like any other XML file.
Then, when you’re ready to test again, right click the project and select reload.
The downside is that’s an awful lot of unloading and reloading. Quite simply, it’s a pain.
Luckily, there’s another way.
What if I put all my “real” targets in a plain ol’ PROJ file, and added it to this dummy project. After a few tests, it was clear that if you do this, you can easily open this “sub-PROJ” project file directly from within the VS IDE, no unloads or reloads necessary. But, I’d need a way to start up my sub project from within the actual dummy VBPROJ file.
Turns out, MSBuild has several actions that essentially let you “fire off” other targets as if they were “subroutines”.
The first I checked out was CallTarget. Unfortunately, it can only call targets within the current project file, and what I really wanted was to fire a target in a different project file.
Then I read up on the MSBuild task. It does allow you to specify both a Project to build (which could be a SLN solution file, but that’s another topic), and a Target within that project. AHA! The answer.
So I setup a simple test. First, my altered dummy VBPROJ file
<?xml version="1.0" encoding="utf-8"?>
<!-- This is a dummy build project for Post Build steps -->
<Project DefaultTargets="Build" ToolsVersion="3.5" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{4D124D21-B683-40F2-89C3-658AA1B51DE9}</ProjectGuid>
<FileAlignment>512</FileAlignment>
<MyType>Empty</MyType>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
</PropertyGroup>
<ItemGroup>
</ItemGroup>
<ItemGroup>
<ProjectsToBuild Include="PostBuild.proj" />
</ItemGroup>
<ItemGroup>
<Folder Include="My Project\" />
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="@(ProjectsToBuild)"/>
</Target>
</Project>
The key element is the ProjectsToBuild item and the MSBuild task of the Build target.
That ProjectsToBuild item should cause the PostBuild.proj file to show up in the VS Solution explorer as just another file you can open like normal and easily edit.
Then, in the PostBuild.proj file,
<?xml version="1.0" encoding="utf-8" ?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
<!-- Must Set MSBuildEmitSolution envvar to 1 to cause msbuild to convert a SLN to a proj file -->
<!-- Define various properties to use during the build here -->
<PropertyGroup>
</PropertyGroup>
<Target Name="Build">
<Exec Command="Echo Did the build" />
</Target>
</Project>
Now, if you build the project from VS and look at the Output window, you show see the “Echo Did the build” item.
Success!
Problems in Paradise
Unfortunately, all is not roses with this approach. As a test, I changed “Did the build” to “Did the build2” in my PostBuild.proj file, then rebuilt the project.
The output didn’t change. At all. Apparently, VS caches the projects in some fashion and only rereads them in peculiar circumstances. I say peculiar because even UNLOADING the project and RELOADING it did not force the PostBuild.proj changes to be read in. Only unloading all of Visual Studio and reloading it worked. This is specifically not what Microsoft alludes to as being correct behavior here, excerpted:
Editing Loaded Project Files
Visual Studio caches the content of project files and files imported by project files. If you edit a loaded project file, Visual Studio will automatically prompt you to reload the project so that the changes take effect. However if you edit a file imported by a loaded project, there will be no reload prompt and you must unload and reload the project manually to make the changes take effect.
But, no matter what I tried, the only thing that seemed to force a reload of cached project files was reloading VS.
Then I happened to notice two other build tasks, Vbc and VCBuild. They wrap the VB and VC compile processes, essentially doing the same thing. But looking at their parameters, they are VERY different beasts. the biggest difference is that Vbc appears to take a list of Source files as an argument, whereas VCBuild is almost identical to MSBuild. It takes Projects and Targets parameters. Hmm, could it be?
I replaced “MSBuild” with “VCBuild” and tried again. Surprisingly, even though my PostBuild.proj file has nothing to do with Visual C, VCBuild happily compiled away, rereading the PostBuild.proj file as I’d wanted. Change the PostBuild.proj file from the IDE, save and Build, and the changes are picked up immediately and compiled just as I was looking for.
The altered dummy VBProj file.
<?xml version="1.0" encoding="utf-8"?>
<!-- This is a dummy build project for Post Build steps -->
<Project DefaultTargets="Build" ToolsVersion="3.5" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{4D124D21-B683-40F2-89C3-658AA1B51DE9}</ProjectGuid>
<FileAlignment>512</FileAlignment>
<MyType>Empty</MyType>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
</PropertyGroup>
<ItemGroup>
</ItemGroup>
<ItemGroup>
<ProjectsToBuild Include="PostBuild.proj" />
</ItemGroup>
<ItemGroup>
<Folder Include="My Project\" />
</ItemGroup>
<Target Name="Build">
<VCBuild Projects="@(ProjectsToBuild)"/>
</Target>
</Project>
But, there’s a problem here too. If you’ve configured the output verbosity level for MSBuild to be something other than the default, VCBuild doesn’t appear to honor the change, and ends up using a fairly minimal output level. That can make debugging far more painful than it should be. None-the-less, it does work.
One more shot
Given that the verbosity level doesn’t follow through, I decided to make one last attempt. Instead of invoking the MSBuild task, why not invoke the MSBuild.EXE itself via an Exec task? Since VS has already invoked MSBuild to perform the outer project build, you can use the MSBuildBinPath property to know where to invoke MSBuild from, so the dummy VBProj file ends up looking like this.
<?xml version="1.0" encoding="utf-8"?>
<!-- This is a dummy build project for Post Build steps -->
<Project DefaultTargets="Build" ToolsVersion="3.5" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{4D124D21-B683-40F2-89C3-658AA1B51DE9}</ProjectGuid>
<FileAlignment>512</FileAlignment>
<MyType>Empty</MyType>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
</PropertyGroup>
<ItemGroup>
</ItemGroup>
<ItemGroup>
<ProjectsToBuild Include="PostBuild.proj" />
</ItemGroup>
<ItemGroup>
<Folder Include="My Project\" />
</ItemGroup>
<Target Name="Build">
<Exec Command='$(MSBuildBinPath)\MSBuild.exe /v:diag "@(ProjectsToBuild)"'/>
</Target>
</Project>
I’ve added the /v:diag switch to force diagnostic level verbosity, but that is easily changeable.
Lo and behold, I get the full output from BOTH MSBuild invocations in the Visual Studio Output window.
But What About Rebuild
At this point, performing a Build from VS will work exactly as expected, but if you try a Rebuild, you’ll get an error about your project file missing a Rebuild target.
In this case, a rebuild is the same as a build, so I’ll just make them dependent tasks.
<Target Name="Build">
<Exec Command='$(MSBuildBinPath)\MSBuild.exe /v:diag "@(ProjectsToBuild)"'/>
</Target>
<Target Name="Rebuild" DependsOnTargets="Build" />
Notice the new Rebuild target above, and that it “Depends On” the Build target. That’s the only change necessary.
Conclusion
At this point, I have a project that I can add to any solution, which itself contains an MSBuild proj file which automatically builds when its host project needs to be built. And this would apply to builds within the IDE as well as builds from TFSBuild or any other automated build system.
Using some conditions and properties, it would be fairly easy to control when the project compiled (for instance, ONLY during a TFSBuild build, or only in the IDE when in RELEASE configuration.
And best of all, it’s all still completely editable from within the Visual Studio IDE!