Under the Hood of MSBuild Integration

by Gael Fraiteur on 17 Jan 2008
All works so magically. Once you have PostSharp installed on your computer, your build process automatically invokes PostSharp when it thinks it should. Most of the cases, it works and it's enough. But if you have a complex build process (especially one that does not use MSBuild), you could ask: how does this work? And how can I integrate PostSharp in my non-MSBuild projects?

Getting Inserted in the MSBuild Process

Most of the stuff is in the file PostSharp-1.0.targets, a file typically installed in the directory C:\Program Files\PostSharp 1.0. From the moment you install PostSharp on your system (that is, using the Microsoft Installer package), this file is included in any project.

There is absolutely no hack in that; it uses extension points of MSBuild especially designed for this purpose (although badly documented). At the very beginning of the file C:\Windows\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets, you will see the following code:

<PropertyGroup>
<CustomBeforeMicrosoftCommonTargets Condition="'$(CustomBeforeMicrosoftCommonTargets)'==''">
   $(MSBuildExtensionsPath)\v2.0\Custom.Before.Microsoft.Common.targets
</CustomBeforeMicrosoftCommonTargets>
<CustomAfterMicrosoftCommonTargets Condition="'$(CustomAfterMicrosoftCommonTargets)'==''">
  $(MSBuildExtensionsPath)\v2.0\Custom.After.Microsoft.Common.targets
</CustomAfterMicrosoftCommonTargets>
<ReportingServicesTargets Condition="'$(ReportingServicesTargets)'==''">
  $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v8.0\ReportingServices\Microsoft.ReportingServices.targets
</ReportingServicesTargets>
</PropertyGroup>
<Import Project="$(CustomBeforeMicrosoftCommonTargets)" 
        Condition="Exists('$(CustomBeforeMicrosoftCommonTargets)')"/>
 

This means that the build process loads the file c:\Program Files\MSBuild\v2.0\Custom.After.Microsoft.Common.targets, if present. He some program wants to be plugged into the standard MSBuild process, it just have to modify this file. And that's exactly what does PostSharp!

Our installer just edits this file and imports our PostSharp-1.0.targets, which gives us the opportunity to insert PostSharp tasks in the build process. How? Here also, using quite standard extension points.

Have a look, in the file Microsoft.Common.targets, at the target Compile. All its dependencies are contained in the property CompileDependsOn. And we want to modify the compilation process, we modify this property from our PostSharp-1.0.targets:

<PropertyGroup>
 <CompileDependsOn>
  $(CompileDependsOn);
  PostSharpInspectConstants;
  PostSharpInspectReferences;
  PostSharp
 </CompileDependsOn>
   </PropertyGroup>

As you can see, we have inserted three targets after other compilation targets: PostSharpInspectConstants, PostSharpInspectReferences, and PostSharp.

Invoking PostSharp Only Relevantly

We could invoke PostSharp in every single project you build on your machine, but it would not last a long time before you uninstall it, isn't it? So we have to decide, in some way, whether PostSharp should be invoked. This is the role of the targets PostSharpInspectConstants and PostSharpInspectReferences. Let's begin with the last one, which is also the most important.

The objective of the PostSharpInspectReferences target is to enable PostSharp whenever the current project is has direct or indirect references to the assembly PostSharp.Public.dll. The heuristic works in most cases: if your assembly references PostSharp.Public.dll, it's probably because it uses an aspect, so it requires to be post-processed. But it does not work always: if you develop a PostSharp plug-in or aspect library, you will surely reference PostSharp.Public.dll, but your plug-in won't need to be transformed. True, but if you develop a plug-in or an aspect library for PostSharp, you are more probable to have read the documentation, and you know how to disable automatic enabling of PostSharp based on references.

And this is one of the role of the target PostSharpInspectConstants: detecting the presence of the constant SkipPostSharp in your project. So if you want to skip PostSharp in your project, or only in a particular build configuration, simply define the SkipPostSharp constant (aka compilation symbol), and you are done!

PostSharp Has Projects, Too

Like MSBuild, PostSharp has its own notion of project: *.psproj files. And like MSBuild, PostSharp projects are based on tasks and plug-ins: a PostSharp project describes all steps that need to be performed to properly analyze and transform assemblies. You cannot just "execute PostSharp against an assembly", you always need to provide it a project.

Hopefully, PostSharp comes with a default project: Default.psproj. The default project automatically detects what is has to do based on custom attributes present in the assembly. That's why the default project is so flexible that most users don't need to be aware of this concept.

But this is an advanced post, so let's go on.

What's important here is not how automatic detection of tasks works (it is documented), but to point out that the default project must take a lot of properties from the environment, in this case from MSBuild. Here are they:

Property Meaning
Output (required) Full path of the output assembly.
ReferenceDirectory Directory by reference to which relative paths in the psproj will be resolved. We pass the root directory of the project (i.e. the directory containing your csproj file).
Configuration Debug or Release, typically. It is not used by Default.psproj.
Platform Any, typically. It is not used by Default.psproj.
SearchPath Comma-separated list of directories that have to be added to the search path while looking for plug-ins or referenced assemblies. We pass the output directory (i.e. bin\debug) as well as all reference paths defined in user-level project properties.
IntermediateDirectory Tells PostSharp where to put its own intermediate stuff. We pass typically obj\debug\PostSharp.
CleanIntermediate Whether PostSharp should clean its intermediate files after execution. We pass false.
MSBuildFullProjectPath Full path of the MSBuild project file (the csproj or vbproj). This is to solve relative paths that users could pass in custom attributes.
SignAssembly Determines whether the assembly should be signed. We take this value from MSBuild.
PrivateKeyLocation Location of the private key. We take this value from MSBuild.

All this stuff is of course passed automatically to PostSharp, so most of the time you don't have to worry about this. However, if you need to invoke PostSharp manually, this will surely interest you.

Invoking PostSharp Manually

If you are not happy with the default integration of PostSharp in the build process, there are many ways to do it differently.

If your project is built using MSBuild, you have the following options:

  • Do not install PostSharp globally, but insert it manually in each project. You will have to import PostSharp-1.0.targets in all C# or VB (or other) project requiring PostSharp. Yup, there is currently no binary package other than the installer performing global registration. But you can do this package yourself by zipping C:\Program Files\PostSharp 1.0 -- simply!.
  • Do not use our MSBuild targets, but use our MSBuild task.
  • Use the command-line utility (see below).

If you don't use MSBuild you can use the command line utility. And there is already a NAnt task in the 1.1 branch!

PostSharp from the Command Line

PostSharp comes with a command line utility. Its syntax is very simple, because nearly everything is passed as named properties:

Usage: postsharp [<options>] <project> <input> [<options>]

Options:
/P:name=value   Sets a property.
/Attach         Gives the opportunity to attach a debugger to the process.

So here is how a call to the command line may look like:

PostSharp.exe 
 Default.psproj 
 .\obj\Debug\PostSharp.Samples.Binding.exe
 /P:Output=.\obj\Debug\PostSharp\PostSharp.Samples.Binding.exe
 /P:ReferenceDirectory=P:\open\branches\1.0\Samples\PostSharp.Samples.Binding
 /P:SearchPath=bin\Debug /P:IntermediateDirectory=obj\Debug\PostSharp
 /P:CleanIntermediate=False
 /P:MSBuildProjectFullPath=P:\\Samples\PostSharp.Samples.Binding\PostSharp.Samples.Binding.csproj

Summary

I've explained how PostSharp is invoked from MSBuild and/or Visual Studio, and described the properties that are passed from MSBuild to PostSharp. Finally, I've shown how to invoke PostSharp on your own, eventually from the command line.

I hope this has been usefull to some of you and...

Happy Aspecting!

-Gael