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?
- Mono Xbuild cannot interpret property functions. These are properties whose values are results of executing an inline function call. E.g.
<!-- Windows specific commands -->
<NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
<PackagesConfig>$([System.IO.Path]::Combine($(ProjectDir), "packages.config"))</PackagesConfig>
- 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
- Xbuild must call NuGet.exe through mono. E.g.
<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:
- 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)
- Using the Configuration Manager, remove unwanted solutions from the configuration.
- 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.
- 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.