Blog Back to all posts View all authors

GitLab Continuous Integration Configuration for Xamarin

by Artur Teodorowicz
February 08. 2019

GitLab Continuous Integration

There is a great variety of commercial solutions on the market that allow you simply configure Continuous Integration in Xamarin project. However, Servocode team refuses to stand by lack of challenging solutions. We have found another way using the tool we work with every day. Let us show you how we face this issue using GitLab!

What’s the aim of this solution? Building apps for Android platform along with triggering all of the Unit Tests. Notice that the configuration will work both for Xamarin.Forms and Xamarin.Android project. That’s how you bring Balance to the Force.

Cake is the anwser

It’s commonly known that you are harder to kidnap with extra cake ballast in your stomach. But did you know that it’s can be your programming weapon? There are many useful tools under open source license in the astonishing word of .NET. Cake is one of them. It’s a cross-platform automatic apps building system. It gives you many possibilities such as project compilation, running tests or restoring NuGet packages.

Cake configuration in our project: small how-to

Ladies and Gentleman, here’s one and only Servocode cake recipe!
Let’s begin with creating a new folder build in the main catalogue of the project and several files that are central to proper working Cake.


How build folder should look like:


└── build ├── build.cake ├── └── tools └── packages.config

Now we can take care of created files. They need some content because cake without the filling is incomplete, isn’t it? Below this text is the necessary content we need to put into the files. Without this step, the configuration would be incomplete.




This is required reference to the NuGet package that contains Cake.

<?xml version="1.0" encoding="utf-8"?> <packages> <package id="Cake" version="0.23.0" /> </packages>

This is required reference to the NuGet package that contains Cake.

And this little script gem boots Cake on Linux. However, its content is not too important during Continuous Integration configuration. In any case of using Cake on Linux, it’s going to look identical. So no worries, you can breathe easy!

# Define directories. SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) TOOLS_DIR=$SCRIPT_DIR/tools ADDINS_DIR=$TOOLS_DIR/Addins MODULES_DIR=$TOOLS_DIR/Modules NUGET_EXE=$TOOLS_DIR/nuget.exe CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe PACKAGES_CONFIG=$TOOLS_DIR/packages.config PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum ADDINS_PACKAGES_CONFIG=$ADDINS_DIR/packages.config MODULES_PACKAGES_CONFIG=$MODULES_DIR/packages.config # Define md5sum or md5 depending on Linux/OSX MD5_EXE= if [[ "$(uname -s)" == "Darwin" ]]; then MD5_EXE="md5 -r" else MD5_EXE="md5sum" fi # Define default arguments. SCRIPT="build.cake" TARGET="Default" CONFIGURATION="Release" VERBOSITY="verbose" DRYRUN= SHOW_VERSION=false SCRIPT_ARGUMENTS=() # Parse arguments. for i in "$@"; do case $1 in -s|--script) SCRIPT="$2"; shift ;; -t|--target) TARGET="$2"; shift ;; -c|--configuration) CONFIGURATION="$2"; shift ;; -v|--verbosity) VERBOSITY="$2"; shift ;; -d|--dryrun) DRYRUN="-dryrun" ;; --version) SHOW_VERSION=true ;; --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;; *) SCRIPT_ARGUMENTS+=("$1") ;; esac shift done # Make sure the tools folder exist. if [ ! -d "$TOOLS_DIR" ]; then mkdir "$TOOLS_DIR" fi # Make sure that packages.config exist. if [ ! -f "$TOOLS_DIR/packages.config" ]; then echo "Downloading packages.config..." curl -Lsfo "$TOOLS_DIR/packages.config" if [ $? -ne 0 ]; then echo "An error occured while downloading packages.config." exit 1 fi fi # Download NuGet if it does not exist. if [ ! -f "$NUGET_EXE" ]; then echo "Downloading NuGet..." curl -Lsfo "$NUGET_EXE" if [ $? -ne 0 ]; then echo "An error occured while downloading nuget.exe." exit 1 fi fi # Restore tools from NuGet. pushd "$TOOLS_DIR" >/dev/null if [ ! -f "$PACKAGES_CONFIG_MD5" ] || [ "$( cat "$PACKAGES_CONFIG_MD5" | sed 's/\r$//' )" != "$( $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' )" ]; then find . -type d ! -name . | xargs rm -rf fi mono "$NUGET_EXE" install -ExcludeVersion if [ $? -ne 0 ]; then echo "Could not restore NuGet tools." exit 1 fi $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' >| "$PACKAGES_CONFIG_MD5" popd >/dev/null # Restore addins from NuGet. if [ -f "$ADDINS_PACKAGES_CONFIG" ]; then pushd "$ADDINS_DIR" >/dev/null mono "$NUGET_EXE" install -ExcludeVersion if [ $? -ne 0 ]; then echo "Could not restore NuGet addins." exit 1 fi popd >/dev/null fi # Restore modules from NuGet. if [ -f "$MODULES_PACKAGES_CONFIG" ]; then pushd "$MODULES_DIR" >/dev/null mono "$NUGET_EXE" install -ExcludeVersion if [ $? -ne 0 ]; then echo "Could not restore NuGet modules." exit 1 fi popd >/dev/null fi # Make sure that Cake has been installed. if [ ! -f "$CAKE_EXE" ]; then echo "Could not find Cake.exe at '$CAKE_EXE'." exit 1 fi # Start Cake if $SHOW_VERSION; then exec mono "$CAKE_EXE" -version else exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -configuration=$CONFIGURATION -target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}" fi


It’s as great as a birthday cake. Why? It contains the whole required configuration prepared for our project. Sounds good, doesn’t it?

#tool nuget:?package=NUnit.ConsoleRunner&version=3.4.0 #addin "Cake.FileHelpers" #addin "Cake.Xamarin" var target = Argument("target", "Default"); var configuration = Argument("configuration", "Debug"); var solutionFile = File("../My_Project.sln"); var androidProject = File("../My_Project/My_Project.Droid/CMP_App.Droid.csproj"); var androidBin = Directory("../My_Project/My_Project.Droid/bin") + Directory(configuration); Task("Clean") .Does(() => { CleanDirectory(androidBin); CleanDirectory(iOSBin); }); Task("Restore-NuGet") .IsDependentOn("Clean") .Does(() => { NuGetRestore(solutionFile); }); Task("Build-Android") .Does(() => { XBuild(androidProject, settings => settings.SetConfiguration(configuration) .WithProperty("AndroidSdkDirectory", "/android/sdk") .WithTarget("SignAndroidPackage")); }); Task("Build-tests") .IsDependentOn("Build-Android") .Does(() => { var parsedSolution = ParseSolution(solutionFile); foreach(var project in parsedSolution.Projects) { if(project.Name.EndsWith("Tests")) { Information("Start Building Test: " + project.Name); XBuild(project.Path, settings => settings.SetConfiguration(configuration)); } } }); Task("Run-unit-tests") .Does(() => { NUnit3("../**/bin/" + configuration + "/*.Tests.dll", new NUnit3Settings { NoResults = true }); }); Task("Default") .IsDependentOn("Run-unit-tests"); RunTarget(target);

Let’s move on to the treatment.

First lines are variables that store data about the path to our projects. In this example, I have used the name My_Project, but you need to change it for the one that fits your own project, so go wild.

The next lines contain tasks that do assigned jobs.



This little guy restores NuGet packages of our project.



It builds our Android app project. Build-Android contains sdk location that lies in the Docker image. However, we will discuss it fully later.



It builds tests projects. That’s all.



And this runs all of the unit tests. All hail to the simplicity!

Taming beasts aka configuration of gitlab-ci.yml file

At this stage, we’re going to configure script used by GitLab’s Runner. To that end, we use ready to go Docker picture named nathansamson/xamarin-android-docker in v25.0.3-20170917 version. It’s compiled with Mono in version and Android SDK V25.




image: "nathansamson/xamarin-android-docker:v25.0.3-20170917" before_script: - "ln -s /android/xamarin/bin/Debug/lib/xbuild/Xamarin /usr/lib/mono/xbuild/Xamarin" stages: - "prepare" - "build" - "test" prepare: cache: key: "$CI_COMMIT_REF_NAME" paths: - "My_Project/packages/**" - "My_Project/build/tools/**" policy: "push" stage: "prepare" script: - "cd build" - "./ --target Restore-NuGet" build: cache: key: "$CI_COMMIT_REF_NAME" untracked: true policy: "pull-push" stage: "build" script: - "cd build" - "./ --target Build-tests" test: cache: key: "$CI_COMMIT_REF_NAME" policy: "pull" untracked: true stage: "test" script: - "cd build" - "./ --target Run-unit-tests"

The before_script section contains symlink to the location of Xamarin installed in the Docker image. It’s required only in this case because Cake looks for installed Xamarin in /android/xamarin/bin/Debug/lib/xbuild/Xamarin automatically. However, in this image, it’s located in /usr/lib/mono/xbuild/Xamarin. If we use different Docker image, this section may be unnecessary for the proper scrip running.

From the level of the gitlab-ci.yml file, we can boot individual tasks declared in build.cake with the small help of the code written below:

cd build && ./ --target <task name>

Next, there are three stages declared in our script. Let’s take a look at them!



It loads all of the NuGet packages for our project and stores them in the storage.



It builds Android apps by creating the .apk fileBuild uses NuGet packages loaded during the earlier stage.



It runs unit tests contained in the project.

The great summary

The above configuration works and does its job in projects we work on. The proof is the number of our satisfied consumers! It’s highly individual, you can make many adjustments, for example, your own Docker image. We are certain it’s going to prove itself in your projects, too. Hours saved on creating working Continuous Integration in Xamarin project in GitLab are priceless. Try it out on your own and have fun!

See also

Communication with the external API in .NET app made simple

Uncle Ben In A Nutshell - Clean Code Principles in C#

contact us

Have an idea ? Let’s talk

Office in Rzeszow
Office in Warsaw


Fill up the form and we will contact you shortly


Company information

Fill up the form and we will contact you shortly.

ServoCode Sp. z o.o.

Jasionka 954E, 36-002 Jasionka, Poland

NIP: 8133719852

REGON: 364182909

KRS: 0000611643

We are using cookies to provide statistics that help us give you the best experience of our site. You can find out more or switch them off if you prefer. However, by continuing to use the site without changing settings, you are agreeing to our use of cookies. Read more