Wednesday, August 14, 2013

Building Your Microsoft Solution with Mono

Background:

The agent for the CloudStack Hyper-V plugin was written in C# using the Microsoft Visual Studio tool chain.

However, Apache CloudStack source should be able to be built using open source tools.

Problem:

How do you compile an ASP.NET MVC4 web app written in C# using an open source tool chain?

Solution:

The latest version of Mono include a tools called Xbuild, which can consume the .sln and .csproj files that Visual Studio generates for your solution.

However, you will have to make some updates to the way your project fetches NuGet dependencies, and the projects that get built.

Let's cover all the steps involved one at a time.

Install Mono

First, install the latest release of Mono 3.x.  This version introduces the support for C# 5.0 and ships with the ASP.NET WebStack (Release Notes)

Although a package for this version is not advertised on the Mono Downloads page, the
mono archives include a 3.0.10 Windows .msi

Alternatively, there is a 3.x Debian package available.  For this package, update your apt sources and use apt-get to install the mono-complete package, which contains the tool chain.  E.g.
sed -e "\$adeb http://debian.meebey.net/experimental/mono /" -i /etc/apt/sources.list
apt-get update
apt-get install mono-complete

Mono ships with an empty certificate store.  The store needs to be populated with common certificates in order for HTTPS to work.  Use the mozroots tool to do this.  E.g.
root@mgmtserver:~/github/cshv3/plugins/hypervisors/hyperv/DotNet# mozroots --import --sync --machine
Mozilla Roots Importer - version 3.0.6.0
Download and import trusted root certificates from Mozilla's MXR.
Copyright 2002, 2003 Motus Technologies. Copyright 2004-2008 Novell. BSD licensed.

Downloading from 'http://mxr.mozilla.org/seamonkey/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1'...
Importing certificates into user store...
Import process completed.

NB: whether you add the certs to your user ( "mozroots --import --sync) or the machine (mozroots --import --sync --machine) depends on what user is used to run web requests.  On Debian 7.0, I found that the machine certificate store had to be updated.

Understand NuGet Packages

NuGet is a package manager for the .NET platform.  These packages consist of assemblies used at compile time and runtime.  The packages are stored in perpetuity on the NuGet website, and fetched by a similarly named command line tool.

Each VisualStudio project lists its NuGet dependencies in the packages.config file, which is in same folder as the project's .csproj file.  E.g.
Administrator@cc-svr10 ~/github/cshv3/plugins/hypervisors/hyperv/DotNet/ServerResource
$ cat ./HypervResource/packages.config
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="AWSSDK" version="1.5.23.0" targetFramework="net45" />
  <package id="DotNetZip" version="1.9.1.8" targetFramework="net45" />
  <package id="log4net" version="2.0.0" targetFramework="net45" />
  <package id="Newtonsoft.Json" version="4.5.11" targetFramework="net45" />
</packages>

By default, your Visual Studio project will search for these assemblies in the packages directory in the folder containing the .sln file.  E.g.
$ cat ./HypervResource/HypervResource.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
...
  <ItemGroup>
    <Reference Include="AWSSDK">
      <HintPath>..\packages\AWSSDK.1.5.23.0\lib\AWSSDK.dll</HintPath>
    </Reference>
...
</Project>

Create a NuGet target

A NuGet target is a build script that downloads packages to the packages folder in advance of compile step of your build.  After this step, the compiler will be able to resolve the assembly dependencies.

Using a NuGet target to automate downloading is a better solution than adding assemblies to your source code assembly or creating a custom script for download (the .sln and .csproj files).  The NuGet target keeps package dependencies at arms length. NuGet carries the legal risk of redistributing other organisations' binaries.  Also, the NuGet target requires much less maintenance work.
Once setup, the NuGet target the list of files to download list the projects being compiled.

To create the target, update your .sln and .csproj files with the NuGet-related tags using Visual
Studio.  Simply open the .sln, and select Project -> Enable NuGet Package Restore.  This creates a .nuget folder with an msbuild task in the NuGet.targets file, its configuration in NuGet.Config, and the command line tool NuGet.exe.  E.g.
Administrator@cc-svr10 ~/github/cshv3/plugins/hypervisors/hyperv/DotNet/ServerResource
$ find ./ | grep NuGet
./.nuget/NuGet.Config
./.nuget/NuGet.exe
./.nuget/NuGet.targets

You will want to add the build script (NuGet.targets) and its configuration (NuGet.Config) to your source code repository.  NuGet.exe should be downloaded separately, as we will explain next.

Update the NuGet target to run with XBuild

Although Mono's Xbuild toll will execute NuGet.targets automatically, Xbuild does not support enough MSBuild tasks to allow it to executed the default NuGet.targets script.

First, you will have to create a script to download NuGet.exe, because Xbuild does not yet support the embedded Task tag (Source).  E.g. if wget is installed, you could use the following instructions:
wget http://nuget.org/nuget.exe
cp nuget.exe ./.nuget/NuGet.exe
chmod a+x ./.nuget/NuGet.exe
Note the change of case.  The code generated NuGet.target expects NuGet.exe

Secondly, if you plan to build on a Windows OS, you will have to update NuGet.targets
to bypass unsupported tags.  Normally, NuGet.targets uses the OS property to bypass unsupported tags automatically.  However, this only works when Mono is used on Linux.  E.g. in the example below an unsupported property function is avoided when the OS is not "Window_NT".
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
    <!-- Windows specific commands -->
    <NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
    <PackagesConfig>$([System.IO.Path]::Combine($(ProjectDir), "packages.config"))</PackagesConfig>
</PropertyGroup>

<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
    <!-- We need to launch nuget.exe with the mono command if we're not on windows -->
    <NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
    <PackagesConfig>packages.config</PackagesConfig>
</PropertyGroup>

To be able to build on Windows, you can either delete the Windows version or revise the conditionals to use a custom property to determine if xbuild is being used.  E.g. in the example below, we
bypass the unsupported function property using a test for the property BuildWithMono.
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
    <!-- Windows specific commands -->
    <!-- <NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath> -->
    <NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
    <!--<PackagesConfig>$([System.IO.Path]::Combine($(ProjectDir), "packages.config"))</PackagesConfig> -->
    <PackagesConfig>packages.config</PackagesConfig>
</PropertyGroup>

<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
    <!-- We need to launch nuget.exe with the mono command if we're not on windows -->
    <NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
    <PackagesConfig>packages.config</PackagesConfig>
</PropertyGroup>

To trigger the bypass, we set buildWithMono when calling Xbuild.  E.g.
xbuild /p:BuildWithMono="true" ServerResource.sln

What code needs bypassing?

  1. Mono Xbuild cannot interpret property functions.  These are properties whose values are results of executing an inline function call. E.g.
  2. <!-- Windows specific commands -->
    <NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
    <PackagesConfig>$([System.IO.Path]::Combine($(ProjectDir), "packages.config"))</PackagesConfig>
  3. Mono does not implement all tasks precisely.  E.g. the Exec tag is missing the LogStandardErrorAsError property available with .NET.  Thus,
    <Exec Command="$(RestoreCommand)"
          LogStandardErrorAsError="true"
          Condition="'$(OS)' == 'Windows_NT' And Exists('$(PackagesConfig)')" />
    causes
    Error executing task Exec: Task does not have property "LogStandardErrorAsError" defined
  4. Xbuild must call NuGet.exe through mono.  E.g.
  5. <NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand>
    <NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 $(NuGetExePath)</NuGetCommand>

Skip unsupported projects

Xbuild cannot compile projects that require additional proprietary assemblies not available through NuGet or the Mono implementation.  For example, unit tests created using Visual Studio Test Tools make use of Visual Studio-specific assemblies.   E.g.
xbuild ServerResource.sln
...
HypervResourceControllerTest.cs(18,17): error CS0234: The type or namespace name `VisualStudio' does not exist in the namespace `Microsoft'. Are you missing an assembly reference?
In this case, HypervResourceControllerTest.cs(18,17) is a reference to Visual Studio test tools:
using Microsoft.VisualStudio.TestTools.UnitTesting;
To create new configuration in you solution that skips compiling these projects follow these steps:
  1. In Visual Studio, create a new configuration that excludes the projects you're not interested in. -(Build -> Configuration Manager..., select on the Active solution platform: drop down list)
  2. Using the Configuration Manager, remove unwanted solutions from the configuration.
  3. Next, close VisualStudio, which will save changes to the .sln and .csproj The .sln will record which projects are associated with the configuration. The .csproj will record the settings of the configuration, such as whether TRACE or DEBUG is defined.
  4. Finally, when calling xbuild assign your configuration's name to the Configuration property.

E.g.
xbuild /p:Configuration="NoUnitTests" /p:BuildWithMono="true" ServerResource.sln
The above will build the projects associated with the NoUnitTests configuration. Source

Final Remarks:

Mono's support keeps getting better with every release.  The quirks discussed in this post may have been addressed by the time you read it.


5 comments :

Leszek Ciesielski said...

Your fixes seem to have already been applied to the current NuGet.targets, but there are still some lingering problems. I guess I'll open another issue with NuGet when I get this to work - so far I got it to fail because of what seems to be a SSL issue on package download.

Leszek Ciesielski said...

Aaand it's waiting for a pull at https://nuget.codeplex.com/SourceControl/network/forks/skolima/NuGetMonoPackageRestore/contribution/5552 . Thanks for you work, definitely pointed me in the right direction. I also had to manually accept NuGet SSL certificates ( as described on http://stackoverflow.com/a/16589218/3205 ) - if I'm not mistaken, they've switched to HTTPS after your post.

Andreas Larsen said...

Great article to a problem that was surprisingly hard to google a solution for.

I found that running "xbuild /p:OS=mono" allowed me to use the .targets file as-is, instead of introducing a new custom property BuildWithMono.

Risky Pathak said...

Thanks this helped me to build my solution on Windows-Mono.

I just commented below line in Nuget.targets

sarah taylor said...

http://www.mobitsolutions.com/