One of the most underrated features in Cake is quite possibly the ability to chain multiple scripts together by using the #load preprocessor directive. I find this feature really helpful in setting up some commonly used Tasks, functions. I also make heavy use of #load ../somefile.cake to centralize and keep consistent my #addin and #tool declarations and the versions in use for each of those.

It's not uncommon for me to have a common cake file like this:

// Tools needed by cake addins
#tool nuget:?package=XamarinComponent&version=1.1.0.49
#tool nuget:?package=ILRepack&version=2.0.13
#tool nuget:?package=Cake.MonoApiTools&version=1.0.10
#tool nuget:?package=Microsoft.DotNet.BuildTools.GenAPI&version=1.0.0-beta-00081
#tool nuget:?package=NUnit.Runners&version=2.6.4

// Cake Addins
#addin nuget:?package=Cake.FileHelpers&version=2.0.0
#addin nuget:?package=Cake.Json&version=2.0.28
#addin nuget:?package=Cake.Yaml&version=2.0.0
#addin nuget:?package=Cake.Xamarin&version=2.0.1
#addin nuget:?package=Cake.XCode&version=3.0.0
#addin nuget:?package=Cake.Xamarin.Build&version=3.0.3
#addin nuget:?package=Cake.Compression&version=0.1.4
#addin nuget:?package=Cake.Android.SdkManager&version=2.0.1
#addin nuget:?package=Cake.Android.Adb&version=2.0.4

You can see we use a lot of Cake addins in our scripts, and in order to keep our CI builds sane, we want to make sure we're pinning very specific versions of each. Without this common file, it's pretty easy for versions of things to get mismatched between cake files, and for things to generally get out of hand quickly, and for CI builds to suddenly stop working when a new version of something is released.

Loading from a URL

Being able to #load arbitrary cake files is great, but as the number of repositories we use cake in grows, the need for a more centralized approach between repositories is becoming increasingly apparent.

One approach is to create a nuget package with the common cake script in it and load the nuget package (and the cake script). This is definitely fine, but it feels a little bit less agile than just fetching a file from an arbitrary url.

Instead, what I really felt I wanted was the ability to do this:

#load url:http://some.com/file.cake

BYO Cake Modules

It turns out, that Cake is architected really really well, and it's actually pretty trivial to make a custom Cake Module to introduce new functionality such as this.

In this specific case, there's an interface ILoadDirectiveProvider that I could implement in my own module. It has the following methods to implement:

  • bool CanLoad(IScriptAnalyzerContext context, LoadReference reference)
  • void Load(IScriptAnalyzerContext context, LoadReference reference)

The interface is pretty self explanatory. I just needed to check each LoadReference passed into CanLoad to decide if it was a url that could be loaded or not (this is why you'll see the url: scheme required - it makes it super easy to determine the intention of the #load directive and whether or not it is a url to be loaded).

My Load implementation ultimately just downloads the URL and saves it to the Urls path (which is ./tools/Urls/ by default), and finally tells the script analyzer context to Analyze the downloaded file.

After implementing this, there's some boiler plate code to register your module with Cake:

[assembly: CakeModule(typeof(UrlLoadDirectiveModule))]

namespace Cake.UrlLoadDirective.Module
{
	public sealed class UrlLoadDirectiveModule : ICakeModule
	{
		public void Register(ICakeContainerRegistrar registrar)
		{
			registrar.RegisterType<UrlLoadDirectiveProvider>().As<ILoadDirectiveProvider>().Singleton();
		}
	}
}

Cake.UrlLoadDirective.Module

I must say, it was pretty painless to create this module. It's currently available on Nuget, and of course it's all open source - check it out on GitHub.

Installation

Installing a Cake Module is pretty simple. Just add a file like this:
./tools/Modules/packages.config

With the content:

<?xml version="1.0" encoding="utf-8"?>
<packages>
	<package id="Cake.UrlLoadDirective.Module" version="1.0.2" />
</packages>

The module will be automatically downloaded and loaded when you run your script, and you're now all set to start using the #load url:... directive!

Thanks!

Thanks to the Cake team for pointing me in the right direction on this one, and for the fantastic architecture of Cake itself to even make this possible.

Have fun #load url:http://ing.your/urls.cake!