MSBuild your SSCLI C# projects

[ Edit 22 Feb 2008: The complete SSCLI.CSharp.targets file can be found here. ]

In my last post I got SSCLI compiling with the 2008 C++ compiler (SSCLI 2.0 and Visual Studio 2008).  (A side note: since it is clearly possible to build SSCLI with the compiler there is no reason you shouldn't be able to get a VS Solution up and running that would build the SSCLI- just a lot of work.)  Having done that I thought it would be interesting to see how plausible actually working in VS with SSCLI projects would be.

As a first step, it ends up that there is relatively little you have to do to get a .csproj that will work with SSCLI through MSBuild.  The key items are:

  1. Set the Csc.exe path.
  2. Make sure the TargetFrameworkDirectory gets set to the SSCLI output directory.

The way to figure this out is to work backwards from the compiler (csc.exe).  Look at what csc's arguments are and look for where arguments get generated for it in the build targets.   

Microsoft.CSharp.targets is the file to start looking in.  It is included at the end of a .csproj so this is a pretty logical place to start looking.  Thankfully the file is short, so it is relatively easy to digest.  (The target files are in the Windows\Microsoft.Net\Framework\v0.0 directories.  I used 3.5 for this article- the last set of MSBuild targets before this version are in 2.0.50727.)

The <Csc> task in Microsoft.CSharp.Targets has a "ToolPath" argument that takes $(CscToolPath).  There is #1!  Finding #2 begins with "References" which is set to @(ReferencePath).  You won't find much in Microsoft.CSharp.Targets so you need to look to the included Microsoft.Common.Targets.

You can figure out #2 by looking at the generation of @(ReferencePaths).  Doing a search will drop you in the ResolveAssemblyReferences target- it's in the comments as an [OUT] -- bingo.  In a csproj referenced .NET assemblies are listed under (logically) <Reference> items.  These are listed here as an [IN] in the comments - see the connection?  (Note: the quick way to look for usage of items is to search for '@(ItemName' -- don't use the closing parenthesis as you'll miss transforms.)

In this target the simple "System" <Reference> is turned into a full @(ReferencePaths) item.  If you look at the <ResolveAssemblyReference> task within this target you'll see some promising parameters-- the most promising being SearchPaths="$(AssemblySearchPaths)".   Look for the source of $(AsemblySearchPaths) and you'll find a nice detailed comment  at the top of Common.Targets:

<!--

  The SearchPaths property is set to find assemblies in the following order:

 

    (1) Files from current project - indicated by {CandidateAssemblyFiles}

    (2) $(ReferencePath) - the reference path property, which comes from the .USER file.

    (3) The hintpath from the referenced item itself, indicated by {HintPathFromItem}.

    (4) The directory of MSBuild's "target" runtime from GetFrameworkPath.

        The "target" runtime folder is the folder of the runtime that MSBuild is a part of.

    (5) Registered assembly folders, indicated by {Registry:*,*,*}

    (6) Legacy registered assembly folders, indicated by {AssemblyFolders}

  (7) Look in the application's output folder (like bin\debug)

  (8) Resolve to the GAC.

  (9) Treat the reference's Include as if it were a real file name.

-->

<AssemblySearchPaths Condition=" '$(AssemblySearchPaths)' == '' ">

       {CandidateAssemblyFiles};

       $(ReferencePath);

       {HintPathFromItem};

       {TargetFrameworkDirectory};

       {Registry:$(FrameworkRegistryBase),$(TargetFrameworkVersion),$(AssemblyFoldersSuffix)$(AssemblyFoldersExConditions)};

       {AssemblyFolders};

       {GAC};

       {RawFileName};

       $(OutDir)

</AssemblySearchPaths>

{TargetFrameworkDirectory} is key here.  If you search for "TargetFrameworkDirectory" you'll find the property shows up in the GetFrameworkPaths and GetReferenceAssemblyPaths targets.  It is the one we want.  It gets replaced by $(TargetFrameworkDirectory) inside of the <ResolveAssemblyReference> task.  You can see this if you use Reflector to look at the task-- but you can probably make that leap without digging further.

That gives us everything we need.  We know where all of our framework (SSCLI) assemblies are so we just need to override Common.targets' path building completely for this and inject our overrides into the regular targets.  You can do this by creating a new target file that includes the Microsoft.CSharp.targets and use this new file instead in your .csproj.   Here's the answer:

<Project InitialTargets="SSCLI_InitialChecks" xmlns="https://schemas.microsoft.com/developer/msbuild/2003">

  <!-- Put this first as this file contains overrides for the standard targets -->

  <Import Project="Microsoft.CSharp.targets" />

 

  <PropertyGroup>

    <!-- Assuming x86 debug build of the framework -->

    <SSCLI_FrameworkPath>$(ROTOR_DIR)\binaries.x86chk.rotor</SSCLI_FrameworkPath>

    <CscToolPath>$(SSCLI_FrameworkPath)</CscToolPath>

    <TargetFrameworkDirectory>$(SSCLI_FrameworkPath)</TargetFrameworkDirectory>

    <!-- This one isn't supported by the sscli csc.exe -->

    <ErrorReport></ErrorReport>

  </PropertyGroup>

 

  <Target

    Name="SSCLI_InitialChecks">

    <Error

      Condition=" '$(ROTOR_DIR)' == '' "

      Text="ROTOR_DIR is not set. Please set an environment variable or property to the SSCLI directory. Note that running 'env' in your SSCLI directory will set this."/>

    <Error

      Condition="!Exists('$(SSCLI_FrameworkPath)')"

      Text="SSCLI not found at expected path: '$(SSCLI_FrameworkPath)' Please build SSCLI if necessary." />

  </Target>

 

  <!--

    Override Microsoft.Common.targets GetFrameworkPaths

  -->

  <Target

    Name="GetFrameworkPaths"

    DependsOnTargets="$(GetFrameworkPathsDependsOn)" />

 

  <!--

    Override Microsoft.Common.targets GetFrameworkPaths

  -->

  <Target

    Name="GetReferenceAssemblyPaths"

    DependsOnTargets="$(GetReferenceAssemblyPathsDependsOn)" />

</Project>

Dump the xml above into a file (say, called "SSCLI.CSharp.targets"), drop it in the same directory as the other target files, and (as already stated) include this file in your csproj instead of Microsoft.CSharp.targets.

Now you can 'msbuild myNiftySscliApp.csproj'.   And yes, you can even open said csproj in Visual Studio and do some developin'.  (With the sad lack of managed debugging, but, hey-- you get Intellisense and all of the other goodies of the IDE.)  In my next posts I'll show you how to grease the wheels as much as you can with the IDE, starting with how to build and run easily, then moving on to making an SSCLI project template.