Best of both worlds: using Grunt to build Xamarin projects on Mac and Windows

Grunt perk in the Fallout: New Vegas

Grunt perk in the Fallout: New Vegas

Couple of weeks ago I opted for Grunt to build my Tesseract.Xamarin Nuget package. Today I will tell you why I decided to take this approach. I will be talking about my own experience and for some of you this information can be obvious but I hope that it will be useful for some of you as well.

Some background

First of all, I should tell why I decided to use Grunt. As you may already know Xamarin projects can be built on either Windows or Mac host but to build a release iOS app you will need a Mac box.

Usually build steps are only preserved in your CI tools. But what happens if CI server fails or it’s database is broken? There is no way to save/load the build steps to/from the version control system. So working on my open-source project I was focused on Mac build and was using this simple bash script to automate the process:

#!/bin/bash

git submodule init

git submodule update --recursive

mono --runtime=v4.0 nuget/NuGet.exe restore Tesseract.Xamarin.sln

xbuild /p:Configuration=Release Tesseract.Xamarin.sln

mono --runtime=v4.0 nuget/NuGet.exe pack Xamarin.Tesseract.nuspec

As you see there is not so much to do. Update the submodule containing Nuget.exe, restore some nuget packages, build the solution, pack the artefacts. But this code as you can easily guess works only on Mac machines and  this fact creates some inequality and you should probably agree with me that any kind of inequality is a bad thing. Ok, let’s make odds even.

Preparing the project

Grunt and Grunt plugins are installed and managed via npm, the Node.js package manager. So you will need to have npm installed on your machine. You will also need to have Git installed and added to the system PATH.

Then you will need to create your package definition by adding package.json file into the root folder. This can be easily done in interactive mode by running npm init command. It is even easier if you project is hosted on GitHub. Then some of the data will be populated into the package.json from the GitHub repository for you.

{
  "name": "tesseract-xamarin",
  "version": "0.3.1",
  "description": "",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/halkar/Tesseract.Xamarin.git"
  },
  "keywords": [
    "tesseract",
    "xamarin"
  ],
  "author": "Artur Shamsutdinov",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/halkar/Tesseract.Xamarin/issues"
  },
  "homepage": "https://github.com/halkar/Tesseract.Xamarin#readme"
}

Grunt gives all the required tools to build a Xamarin project either on Mac or on Windows machine but I’m using only few of them – grunt itself, grunt-msbuild to build the project, grunt-nuget to handle all the Nuget operations and grunt-dotnet-assembly-info to update assembly version.

npm install grunt --save-dev
npm install grunt-msbuild --save-dev
npm install grunt-nuget --save-dev
npm install grunt-dotnet-assembly-info --save-dev

After running these commands plugins will be installed locally and added into your package.json as devDependencies.

Usually you also need to install grunt-cli plugin globally to run Grunt from the command line. But I prefer a different approach. Let’s add one more small script to the  package.json to simplify build process.

"scripts": {
  "test": "node -e \"require('grunt').cli();\""
}

Complete file.

{
  "name": "tesseract-xamarin",
  "version": "0.3.1",
  "description": "",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/halkar/Tesseract.Xamarin.git"
  },
  "scripts": {
    "test": "node -e \"require('grunt').cli();\""
  },
  "keywords": [
    "tesseract",
    "xamarin"
  ],
  "author": "Artur Shamsutdinov",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/halkar/Tesseract.Xamarin/issues"
  },
  "homepage": "https://github.com/halkar/Tesseract.Xamarin#readme",
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-dotnet-assembly-info": "^1.0.19",
    "grunt-msbuild": "^0.3.4",
    "grunt-nuget": "^0.1.4"
  }
}

Building Xamarin project with JavaScript

To setup Grunt build you will need to add Gruntfile.js. Here is my version.

module.exports = function (grunt) {
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        msbuild: {
            release: {
                src: 'Tesseract.Xamarin.sln',
                options: {
                    projectConfiguration: 'Release'
                }
            }
        },
        nugetrestore: {
            restore: {
                src: 'Tesseract.Xamarin.sln',
                dest: 'packages/'
            }
        },
        nugetpack: {
            dist: {
                src: 'Xamarin.Tesseract.nuspec',
                dest: './',
                options: {
                    version: '<%= pkg.version %>'
                }
            }
        },
        assemblyinfo: {
            options: {
                files: ['SharedAssemblyInfo.cs'],
                info: {
                    version: '<%= pkg.version %>.0',
                    fileVersion: '<%= pkg.version %>.0'
                }
            }
        }
    });

    grunt.loadNpmTasks('grunt-msbuild');
    grunt.loadNpmTasks('grunt-nuget');
    grunt.loadNpmTasks('grunt-dotnet-assembly-info');

    grunt.registerTask('default', ['nugetrestore:restore', 'assemblyinfo', 'msbuild:release', 'nugetpack:dist']);

};

It has only four tasks, nugetrestore:restore to restore all nuget packages we need, assemblyinfo to update assembly version, msbuild:release to build the project and nugetpack:dist to create the nuget package which will be used to distribute Xamarin.Tesseract. As you can see configuration is uncomplicated and replicates the bash script which I was using before.
One small detail to notice. There is a package version in the package.json file and there is a package version in the Xamarin.Tesseract.nuspec (also I would like my libraries to have same version). As nobody likes duplications I will be using version from the package.json for my nuget package. To use it I read package data

pkg: grunt.file.readJSON('package.json')

and then use package version in the nugetpack:dist task

nugetpack: {
  dist: {
    src: 'Xamarin.Tesseract.nuspec',
    dest: './',
    options: {
      version: '<%= pkg.version %>'
    }
  }
}

and in the assemblyinfo task

assemblyinfo: {
  options: {
    files: ['SharedAssemblyInfo.cs'],
    info: {
      version: '<%= pkg.version %>.0',
      fileVersion: '<%= pkg.version %>.0'
    }
  }
}

Now you can build the project after cloning the repository by running just two commands.

npm install
npm test

And the best part is that now you can use the same steps to build the project on either Mac or Windows without any changes. On Windows you will need to open Visual Studio once to establish connection with the Mac build host. Another positive thing is that I don’t need the nuget submodule anymore as grunt-nuget brings Nuget.exe and deploys it locally.

My personal outcomes

As a reusable code fanatic I’m now happy with my build process. I can extend it using hundreds of Grunt plugins with both platforms supported. For example, run unit tests with grunt-nunit-runner, automatically push new version to the Nuget server with grunt-nuget and even publish your app in HockeyApp with grunt-hockeyapp-upload. I can shuffle build steps without touching the CI server and my overcomplicated build configuration won’t be decayed even if server goes off. Configuration is saved in the VCS which means it can be rolled back to any previous version and deployed to any number of servers.

So I’ve found my paragon and what do you use to build your projects?

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s