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:
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 call
method.
In Ruby, we do not need parentheses to pass arguments to a method, so foo
becomes:
Finally, we can use a symbol instead of a string for the name
argument.
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:
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
.
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 second_lane
from first_lane
.
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:
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.
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 second_lane
from first_lane
.
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.
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).
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:
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:
- fastlane uses Ruby method calls to create its DSL
- it uses the helpful
method_missing
Ruby 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 lane
called secure_lane
that will ensure all parameters passed to a lane are non optional.