Overview
There needs to be a way for
MSBuild Extenders (component providers) to install custom targets, tasks and logger files. Today the only way an
MSBuild component provider can do this, is by dropping these extensions in the same directory as
MSBuild was installed. Component Providers can then access these files by using the
$(MSBuildBinPath) reserved property.
For various reasons discussed in this spec, using
$(MSBuildBinPath) is not a good practice. This spec discusses these scenarios as well as a design which will allow component providers to install their components in a well known location without loosing all the portability benefits of having them in a centralized location.
Scenarios
Component Vendor
Visual Studio Tools for Office (VSTO) is using
MSBuild as their build platform. The VSTO build process uses
@$(MSBuildBinPath)\Microsoft.<Language>.targets@ as a baseline but they need to augment that process with their own
special sauce. VSTO's special sauce is put in a separate set of targets files called Microsoft.VSTO.targets. All VSTO templates must import the standard @Microsoft.<Language>.Targets@ as well as the Microsoft.VSTO.targets. With this in mind VSTO templates look something like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="????\Microsoft.VSTO.targets" />
</Project>
As a component provider and extender of
MSBuild where does VSTO place their targets so that templates can auto-include them in the same way as standard targets are included. Two choices come to mind:
* VSTO could place their target files in the redist directory and their templates could say
@$(MSBuildBinPath)\Microsoft.VSTO.targets@. This however is very bad since a) the redist directory could go into System File Protection and more importantly b) it is a very bad thing to install components in a directory you have no control over. Any QFE, fundamental change or re-install of our components may step on anything that VSTO has done.
* VSTO could somehow place their components in some well known location on disk which in turn could be discovered by
MSBuild. Option 2 is clearly the right choice. This spec addresses where that special well known location
is. Once this spec is implemented the VSTO templates would look something like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath)\VisualStudio\v8.0\VSTO\Microsoft.VSTO.targets" />
</Project>
Internal Microsoft Build Environment (Razzle)
Visual Studio Tools for Office (VSTO) also uses their templates for internal development. Under VSTO's enterprise build environment all tools and respective dependencies must be checked into
SCCs depot root and they must be isolated from anything else going on in the machine. Problem arises because all the VSTO templates are defined as:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath)\VisualStudio\v8.0\VSTO\Microsoft.VSTO.targets" />
</Project>
However when running in a "razzle" like environment VSTO cannot have
$(MSBUildExtensionsPath) resolving to c:\Program
Files\MSBuild. They instead need
$(MSBuildExtensionsPath) resolving into
$(DepotRoot)\Tools\. Once this spec is implemented VSTO can easily achieve this by overriding the
$(MSBuildExtensionsPath) property just like they would override any other
MSBuild property. In this scenario VSTO would build internal projects like this:
[MSBuild] [InternalVSTOProject.csproj] [/p:MSBuildExtensionsPath=$(DepotRoot)\Tools\]
Design
MSBuild will support one new
reserved property:
MSBuildExtensionsPath MSBuildExtensionsPath will resolve to
@System.Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)\MSBuild@. Thus, in a standard Windows installation (where standard is defined as $(windir) == c:\Windows)
MSBuildExtensionsPath will point to:
http://alexkipman.members.winisp.net/channel9wikiimages/wikibeta2dcr01.jpg
This property will then be consumable from within any project file just like any other
MSBuild property as exemplified by the example below:
Imagine a file called Extensions.proj:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
<Target Name="A">
<Message Text="MSBuildExtensionsPath = [$(MSBuildExtensionsPath)] " />
</Target>
</Project>
When executed the above file would yield:
C:\test>msbuild extensions.proj
Microsoft (R) Build Engine Version 2.0.40607.45
[Microsoft .NET Framework, Version 2.0.40607.45]
Copyright ""(C)"" Microsoft Corporation 2004. All rights reserved.
Target "A" in project "extensions.proj"
[MSBuildExtensionsPath] = C:\Program [Files\MSBuild]
Special Design Notes
001 - Overridibility
Today
all MSBuild reserved properties
are not overridible. This was a deliberate design decision since the values of these variables should never be changed. For example:
*
@MSBuildBinPath@ is
always the directory where Microsoft.Build.Engine.dll lives.
*
@MSBuildProjectPath@ is
always the directory where the current project file is executing from
* etc
@MSBuildExtensionsPath@ does not meet the above criteria. It is very interesting (based on razzle scenarios I'd venture it's a requirement) that end-users have the ability to control what
@MSBuildExtensionsPath@ resolves into.
In order to accomplish this
and not invent new magic, we are going to treat
@MSBuildExtensionsPath@ like any other property (ie our existing property precedence rules apply). Today our precedence rules are as follows:
* Environment Variables
* Last Property declaration within a Project File
* Last command line Property declaration
* Reserved Property
@MSBuildExtensionsPath@ will be created as the lowest precedence property, meaning that even an environment variable will be able to override it's default value of @C:\Program
Files\MSBuild@. At the risk of being pedantic, the precedence rules for
@MSBuildExtensionsPath@ will look like this:
*
$(MSBuildExtensionsPath) * Environment Variables
* Last Property declaration within a Project File
* Last command line Property declaration
* Reserved Property
002 - What to do when the directory does not exist
MSBuild continues to have a 0 installation requirement. As such our existing shipping vehicles, namely the .NET redistributable, will not be creating @C:\Program
Files\MSBuild@ on disk. Component providers and end-users are responsible for creating and appending to @C:\Program
Files\MSBuild@. Because
MSBuild will not be creating this directory on disk, it is entirely possible that someone may
use $(MSBuildExtensionsPath) without it actually existing on disk.
MSBuild will not be doing any special checking for existence on the directory. The
MSBuild Engine will always resolve it to a string, and if that string, or the combination of that string with a file name, results in a non-existing location on disk no special error message will be fired.
Things to keep in mind as a Component Vendor
Component vendors own their own infrastructure underneath
$(MSBuildExtensionsPath). The following are merely recommended best practices / patterns of usage:
Things to keep in mind
*
DO Create a single folder for your company
*
DO NOT Put target and task assemblies directly underneath
$(MSBuildExtensionsPath) *
DO Think about versioning of your target and task assembly files. Either create a folder with the version name, or somehow embed the version in the target and assembly file itself
*
DO Create sub folders underneath the company node if your company ships different sets of target and task assemblies which will ultimately version independently
As an example this is potentially what the folder structure would look like for Microsoft as a provider of target and task assemblies:
http://alexkipman.members.winisp.net/channel9wikiimages/wikibeta2dcr02.jpg
Open Issues
None
Closed Issues
Issue #001
Question: Why not just use registry keys for this? Something akin to how packages / sdk's register their assemblies in Assembly folders EX.
Answer: We considered this option at length. Once all was said and done this option was discounted mainly over
complexity. Seemed like too much architecture for the problem. It requires us to prescribe more, educate component vendors more and implement more features in the engine. This option although more flexible carries with it a higher cost. Another reason we ended up staying away from this option is one of xcopy deployment. Having all these files in a well known location on disk makes it easy to deploy things across different machines. We can just xcopy all of
$(MSBuildExtensionsPath) from machine A to machine B and stuff just works. Having registry keys means that these files would be scattered all over the disk and customers (end-users) would have no way to easily port these files from machine A to machine B.
Issue #002
Question: Why not just use environment variables for this?
Answer: We actually did consider this option for some time. There are various reasons we finally decided to discount that solution. 1) Environment variables are fragile. It is very easy for people to override them, or even pre-pend or post-pend information to it almost by accident. We felt this solution was too fragile for component vendors. 2) We felt that forcing component vendors to set system wide environment variables as our recommended way to solve this problem was not good. 3) Environment variables don't version really well, and it is difficult to ensure that no other component vendor will step on you. 4) Finally the consensus was that having a fundamental piece of the puzzle rely on setting system wide environment variables was just not .Netty enough.
Issue #003
Question: What about having a way to register
magic properties with
MSBuild like
$(MSBuildBinPath)?Answer: We thought about this at length and decided
not to allow it. We think the current property infrastructure is rich and expressive enough to solve most if not all scenarios. People can register properties as environment variables, as well as within project and target files. Environment variables are stored in specific ways in the registry which in a way implies that you can register properties in the registry as well. Besides
$(MSBuildExtensionsPath) we could not come up with any compelling scenarios for allowing component providers to register
magic properties within
MSBuild. Because we already solved the
$(MSBuildExtensionsPath) problem through the above design, we decided to disallow this scenario. If we receive abundant customer feedback to the contrary, we can always add fancier technology in this space in V3.0 of
MSBuild
Issue #004
Question: What about having *.tasks automagically registered with
MSBuild if they appear under
$(MSBuildExtensionsPath)Answer: We thought about this at length and decided
not to allow it. The reason for this is simple. There are scenarios, particularly around large enterprises, where you want to have all your targets and tasks in a specific location relative to the SCC depot root. Under those circumstances you do not want the
MSBuild Engine to search and load tasks that are outside of that depot structure. Imagine the scenario where some 3rd party component legitimately installs a
CompanyName.Technology.tasks file under
$(MSBuildExtensionsPath)\CompanyName\V1.0\CompanyName.tasks. Now imagine there is a task in that file which conflicts with a task being used in one of my Enterprise projects. I would run into a case where a task was being loaded from a location that I don't want to load it from. This is not hard to diagnose and fix under diagnostic mode, however the user doesn't have an easy way to get out of this state short of removing the
CompanyName.tasks file from disk, which in turn may break some other legitimate scenario. The proper way of solving this problem would be for us to provide users with a way to disable auto-registration of ""*"".tasks files contained underneath
$(MSBuildExtensionsPath). Because we don't really have good scenarios for customers adding
.tasks files under $(MSBuildExtensionsPath) we decided to keep things simple. We'll not auto-pick """".tasks files from within
$(MSBuildExtensionsPath), and if we receive abundant customer feedback to the contrary, we can always add fancier technology in this space in V2.0 of
MSBuild.