Implementing fastlane in 50 lines of code

Fastlane is a tool used by almost every mobile developer. The main advantages are its simplicity, the time it saves us and the fact that it’s built on top of Ruby. But we tend to take this tool for granted. Today I want to present how fastlane works, internally, and how it takes advantage of Ruby.

Let’s take a clean sheet of paper, and rebuild its core component from scratch.

Fastlane DSL

Fastlane uses a Domain Specific Language to define its lanes.

For instance let’s look at a lane definition:

lane :first_lane do
  puts "first_lane"
end

It looks like a Ruby method definition (there is a name and a body) but it is not exactly the same. Let’s first understand how such a definition is possible.

A minimal Ruby method is defined like this:

def foo
end

foo # returns nothing right now

To pass parameters to a method, we can do it like so:

def foo(name)
  puts name
end

foo("bar") # prints bar

Ruby also accepts blocks of code as parameters. These blocks (or closures in other languages) can be stored and executed with the call method.

def foo(name, &block)
  block.call
end

# the two following forms are correct to define blocks

foo("bar") { puts "bar" } # prints bar

foo("bar") do
  puts "bar"
end # prints bar

In Ruby, we do not need parentheses to pass arguments to a method, so foo becomes:

foo "bar" do
  puts "bar"
end # prints bar

Finally, we can use a symbol instead of a string for the name argument.

foo :bar do
  puts "bar"
end

Attentive readers will notice that this call to foo is really similar to how we define lanes in a Fastfile. Renaming foo to lane we end up with:

def lane(name, &block)
  block.call
end

lane :first_lane do
  puts "first_lane"
end

We have just demonstrated that a lane definition is plain old Ruby behind the scene. There is no magic nor complex parsing, it’s a simple method call. In reality, when you write your Fastfile, the methods lane or private_lane are hidden from you but there anyway.

Storing lanes

For now our lane first_lane is executed immediately. We want a way to store a lane and to call its block later when we need it.

First, let’s declare a Lane class that will store both the lane name and block.

class Lane
  # defines a getter method for the instance variable name
  attr_accessor :name

  def initialize(name, block)
    @name = name
    @block = block
  end

  def call
    @block.call
  end
end

Now in our lane method, we can create a Lane instance and return it.

def lane(name, &block)
  Lane.new(name, block)
end

first = lane :first_lane do
  puts "first_lane"
end

# nothing is executed for now, until...
first.call # prints "first_lane"

To define multiple lanes, it would be nicer to create some object to store them all. Let’s create a Runner class that will store all the lanes defined, and expose a method execute to call a specific lane by its name.

class Runner
  def initialize
    @lanes = []
  end

  def add(lane)
    @lanes << lane
  end

  # Find the lane that matches lane_name and call its block
  def execute(lane_name)
    lane = @lanes.find { |l| l.name == lane_name }
    lane.call
  end
end

Now that we have this runner at our disposal, let’s use it to store and call our custom lanes:

RUNNER = Runner.new

def lane(name, &block)
  RUNNER.add(Lane.new(name, block))
end

lane :first_lane do
  puts "first_lane"
end

lane :second_lane do
  puts "second_lane"
end

RUNNER.execute(:first_lane) # prints "first_lane"
RUNNER.execute(:second_lane) # prints "second_lane"

Calling lanes from another lane

Fow now, our implementation is rather trivial and does not allow us to call second_lane from first_lane.

lane :first_lane do
  puts "first_lane"
  second_lane
end

If we try so, we get an error: undefined local variable or method `second_lane`.

When we think about it, it makes complete sense. We never defined a method called second_lane. We just defined a lane named second_lane but we can’t call it like this. We need our runner to execute it. But how can we invoke our runner in such a case?

To fix this issue, let’s look at a helpful feature Ruby provides. If we take a class Foo with no methods at all and call the method bar on an instance of Foo, we will get the same error as before, stating the bar method is undefined.

class Foo
end

foo = Foo.new
foo.bar # undefined method `bar`

Ruby gives us a way to catch this undefined method error. We can implement the method_missing method in the Foo class, and that gives us an opportunity to execute some code in the case of an undefined method. For instance:

class Foo
  def method_missing(method_sym)
    puts "method_missing: #{method_sym}"
  end
end

foo = Foo.new
foo.bar # prints "method_missing: bar"

The undefined method error is gone. We can call anything on foo, we will pass in the method_missing.

With this trick up our sleeve, back to our lanes. We saw how method_missing can be helpful, but we need to implement this method in a class. So let’s create a FastFile class that will wrap our runner and our lanes definitions.

class FastFile
  def initialize
    @runner = Runner.new
  end

  def lane(name, &block)
    @runner.add(Lane.new(name, block))
  end

  def ___
    lane :first_lane do
      puts "first_lane"
    end

    lane :second_lane do
      puts "second_lane"
    end
  end
end

Note that we created a method ___. That’s temporary, just to take a moment to think about what it should do.

First this method will list the lanes we defined (the equivalent of a Fastfile). And second it will execute a lane as an entry point. Indeed, when we run fastlane scan in our terminal, scan is the entry point. That’s the single lane fastlane will execute, all the other lanes called during the execution of scan are internal.

So this method ___ could be renamed run(lane_name), and take as parameter the lane name to execute as the entry point.

class FastFile
  # ...

  def run(lane_name)
    lane :first_lane do
      puts "first_lane"
    end

    lane :second_lane do
      puts "second_lane"
    end

    @runner.execute(lane_name)
  end
end

fastfile = Fastfile.new
fastfile.run(:first_lane)

Now that we have a nice class around our lane definitions, let’s try again to call second_lane from first_lane.

class FastFile
  # ...

  def run(lane_name)
    lane :first_lane do
      puts "first_lane"
      second_lane
    end

    ...
  end
end

We get the same error as before undefined local variable or method `second_lane`, but this time, we are in a class, so we can implement method_missing. The idea here is to catch the undefined method error with method_missing where method_sym equals to second_lane. And then we can use our runner to execute the lane second_lane from its name.

class FastFile
  # ...

  def run(lane_name)
    lane :first_lane do
      puts "first_lane"
      second_lane
    end

    lane :second_lane do
      puts "second_lane"
    end

    @runner.execute(lane_name)
  end

  def method_missing(method_sym)
    @runner.execute(method_sym)
  end
end

fastfile = Fastfile.new
fastfile.run(:first_lane) # prints "first_lane\nsecond_lane"

And that does the trick, the error is gone and second_lane has been called from first_lane.

Creating a Fastfile

All works fine but we can improve things a little bit. For now the lanes are defined in the run method, but that’s not elegant. We want to define all those lanes in a separate file called a Fastfile. This way the DSL in the Fastfile is separated from the fastlane gem (in our case, our single ruby file).

So let’s create a Fastfile with our custom lanes (the name of file does not matter but we follow fastlane convention).

lane :first_lane do
  puts "first_lane"
  second_lane
end

lane :second_lane do
  puts "second_lane"
end

Now in our run method we need to execute the content of this Fastfile, because the lanes are not longer defined.

We will first read the content of the Fastfile with File.read then evaluate its content. Ruby provides a method eval that evaluates some Ruby string at runtime. This is perfect for what we want, instead of defining our lanes in the run method, we will write them in another file and evaluate the content of this file dynamically.

The method run becomes:

class FastFile
  # ...

  def run(lane_name)
    content = File.read("Fastfile")
    eval(content)
    @runner.execute(lane_name)
  end

  # ...
end

Note: keep in mind that the eval method evaluates the Ruby content within the context of the FastFile class. This is important to remember as it can lead to weird behaviors. For instance, you can’t define a String extension in your Fastfile the same way you would normally do in a Ruby file.

Conclusion

We made it! We recreated the behavior of fastlane. On one side a Fastfile where we define our lanes for our project. On the other side, our Ruby script that executes our lanes. Of course the real fastlane gem is a far more advanced (with error handling, hooks, actions, etc), but the core idea is here.

The few things to remembers are:

With this new understanding, you can extend the fastlane DSL the way you want. For instance, it’s really simple to implement a replacement of lane called secure_lane that will ensure all parameters passed to a lane are non optional.