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
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.
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.
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
├── build.sh
└── 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" https://cakebuild.net/download/bootstrapper/packages
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" https://dist.nuget.org/win-x86-commandline/latest/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!
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 7.3.99.59 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" - "./build.sh --target Restore-NuGet" build: cache: key: "$CI_COMMIT_REF_NAME" untracked: true policy: "pull-push" stage: "build" script: - "cd build" - "./build.sh --target Build-tests" test: cache: key: "$CI_COMMIT_REF_NAME" policy: "pull" untracked: true stage: "test" script: - "cd build" - "./build.sh --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 && ./build.sh --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 file. Build uses NuGet packages loaded during the earlier stage.
It runs unit tests contained in the project.
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!
Have an idea ? Let’s talk
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
Your message has been sent!
Your message has been sent!