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 uses a Domain Specific Language to define its lanes.
For instance let’s look at a lane definition:
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:
To pass parameters to a method, we can do it like so:
Ruby also accepts blocks of code as parameters. These blocks (or closures in other languages) can be stored and executed with the
In Ruby, we do not need parentheses to pass arguments to a method, so
Finally, we can use a symbol instead of a string for the
Attentive readers will notice that this call to
foo is really similar to how we define lanes in a Fastfile. Renaming
lane we end up with:
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
private_lane are hidden from you but there anyway.
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
Now in our
lane method, we can create a
Lane instance and return it.
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.
Now that we have this runner at our disposal, let’s use it to store and call our custom lanes:
Calling lanes from another lane
Fow now, our implementation is rather trivial and does not allow us to call
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.
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:
undefined method error is gone. We can call anything on
foo, we will pass in the
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.
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.
Now that we have a nice class around our lane definitions, let’s try again to call
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_sym equals to
second_lane. And then we can use our runner to execute the lane
second_lane from its name.
And that does the trick, the error is gone and
second_lane has been called from
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).
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.
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.
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:
- fastlane uses Ruby method calls to create its DSL
- it uses the helpful
method_missingRuby method for lanes to call one another
- the content of the Fastfile is evaluated at runtime within a specific context
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
secure_lane that will ensure all parameters passed to a lane are non optional.