Last week during a code review at work, this thing became a topic again. This time I’ve decided to write a post about it so other colleagues can learn from it and be aware of the potential risks it implies to define methods in your rake files. The next time I need to refer to this post I will be able to come back here and have everything explained.
In my case I learned it the hard way. Some time ago, we defined a method called logger
to initialize and memoize a Logger
instance that should write to a task’s dedicated log file. The surprise came when we realized that instead of writing log messages to the file we had conceived for this task, it logged everything to another file. It turned out to be that we had already defined another method named logger
in a different *.rake file that pointed to other log file!
Lets see what happens when you define methods with the same name in rake tasks with the following example.
Image we have a Rakefile
that defines some rake tasks. To make it a little more real, instead of defining the tasks inside the Rakefile
, we will have them in two separate files called foo.rake
and bar.rake
, yes, I know I am super original with naming 😀
1 2 3 4 |
# Load tasks Dir.glob('*.rake').each do |path| load path end |
1 2 3 4 5 6 7 8 9 |
desc 'bar task' task :bar do puts 'In bar task' greet end def greet puts 'Hello from bar.rake' end |
1 2 3 4 5 6 7 8 9 |
desc 'foo task' task :foo do puts 'In foo task' greet end def greet puts 'Hello from foo.rake' end |
We use the Rakefile
to load the contents of the *.rake files. In each *.rake file we define two things: a method called greet
, and a task that uses this method. Lets check first that everything is defined and that all tasks are available.
Open a terminal inside this “project” and run:
1 2 3 |
2.4.3 ruby/rake_example $ rake -T rake bar # bar task rake foo # foo task |
And now run each task:
1 2 3 4 5 6 |
2.4.3 ruby/rake_example $ rake foo In foo task Hello from foo.rake 2.4.3 ruby/rake_example $ rake bar In bar task Hello from foo.rake |
WAT? Everyone (or at least me :D) would expect to have seen “Hello from bar.rake!” when running rake bar
, but instead, the greet
method definition at foo.rake
prevailed over the one inside bar.rake
.
Having namespaces doesn’t solve the issue
In the last example, I used a very simple setup with no namespaces, but when I faced the error that made me learn this, it had namespaces. Lets put some namespaces to the example above, leaving the Rakefile
as it was, and see what happens. Note I have placed the definition of greet
inside each namespace.
1 2 3 4 5 6 7 8 9 10 11 |
namespace :bar_namespace do desc 'bar task' task :bar do puts 'In bar task' greet end def greet puts 'Hello from bar.rake' end end |
1 2 3 4 5 6 7 8 9 10 11 |
namespace :foo_namespace do desc 'foo task' task :foo do puts 'In foo task' greet end def greet puts 'Hello from foo.rake' end end |
1 2 3 |
2.4.3 ruby/rake_example $ rake -T rake bar_namespace:bar # bar task rake foo_namespace:foo # foo task |
Now everything is within a namespace, lets run each task again, this time prefixed with the corresponding namespace:
1 2 3 4 5 6 |
2.4.3 ruby/rake_example $ rake foo_namespace:foo In foo task Hello from foo.rake 2.4.3 ruby/rake_example $ rake bar_namespace:bar In bar task Hello from foo.rake |
As you can see, rake namespaces don’t help us isolating the methods we have defined inside. They have no effect for this purpose. What can we do then?
How to work around this?
If you though about naming each task differently, I hope you don’t trust your eloquence that much, mine has been proven to fail. Thinking about using a different name each time is a bad idea, and the sooner or later names will clash. Lets see the alternatives I suggested to my peer during that code review:
1. Extract your methods to some library
If your methods have enough coherence to be grouped together or you plan to reuse them elsewhere, you may consider extracting them to some module. Once extracted, invoke them inside your rake tasks. Here we also have a great chance to add unit tests for our new extracted classes. Lets see it applied to our example:
1 2 3 4 5 6 7 8 9 10 11 |
class Greeter attr_reader :filename def initialize(filename: '') @filename = filename end def greet puts "Hello from #{filename}" end end |
1 2 3 4 5 6 7 |
require_relative 'greeter' desc 'bar task' task :bar do puts 'In bar task' Greeter.new(filename: 'bar.rake').greet end |
1 2 3 4 5 6 7 |
require_relative 'greeter' desc 'foo task' task :foo do puts 'In foo task' Greeter.new(filename: 'foo.rake').greet end |
If we run it, we now get what we expected:
1 2 3 4 5 6 |
2.4.3 ruby/rake_example $ rake bar In bar task Hello from bar.rake 2.4.3 ruby/rake_example $ rake foo In foo task Hello from foo.rake |
2. Put everything inline
Sometimes taking the easy path in life is OK. If none of the reasons we mentioned above for extracting the methods apply, it is perfectly fine to go simple and put your methods inline. In our example that would look as it follows:
1 2 3 4 5 |
desc 'bar task' task :bar do puts 'In bar task' puts 'Hello from bar.rake' end |
1 2 3 4 5 |
desc 'foo task' task :foo do puts 'In foo task' puts 'Hello from foo.rake' end |
And if we run them, we get again what we expected:
1 2 3 4 5 6 |
2.4.3 ruby/rake_example $ rake bar In bar task Hello from bar.rake 2.4.3 ruby/rake_example $ rake foo In foo task Hello from foo.rake |
3. Define methods inside task block (Edit June 12th 2019)
This one is a suggestion from Chris Drit, one of my coworkers at Spin, as he needed to define methods to invoke them recursively later. Thanks! It turns out you can also avoid this problem by defining methods inside of the task block as it follows:
1 2 3 4 5 6 7 8 9 10 11 |
namespace :bar_namespace do desc 'bar task' task :bar do def greet puts 'Hello from bar.rake' end puts 'In bar task' greet end end |
1 2 3 4 5 6 7 8 9 10 11 |
namespace :foo_namespace do desc 'foo task' task :foo do def greet puts 'Hello from foo.rake' end puts 'In foo task' greet end end |
Now let’s run it:
1 2 3 4 5 6 |
2.6.1 ruby/rake_example $ rake foo_namespace:foo In foo task Hello from foo.rake 2.6.1 ruby/rake_example $ rake bar_namespace:bar In bar task Hello from bar.rake |
(End edit June 12th 2019)
In order to quickly explain this topic, I have used a simple rake setting. If you try this within a Rails application, the result will be the same, is a rake issue, in fact, when I first experienced this error, it was inside a Rails application.
If you liked this post or it helped you to save time, please share it with your friends and peers or maybe send me a tweet to make me smile 🙂 Two months ago I still got some “thank you” tweets because of a Java post I wrote about years ago, it felt incredibly great to know it stills help other people! See you next week!