100 Languages Speedrun: Episode 17: Rake
Once upon a time, people were coding in C, and building a full program was a convoluted process taking many steps. Make) was created to manage dependencies, and to run just the needed commands.
Newer languages generally don't require such convoluted build system, but Make-style dependency managers have been adopted for a lot of different situations.
The original Make is honestly a terrible mess of shell code held together by some flimsy duct tape, and nobody should ever use it, but it led to creation of many much better systems, including Ruby-based Rake this episode is about. Rake is included in Ruby on Rails, but you can and should also use it for any other projects, even projects that aren't Ruby-based.
Hello, World!
Rake is very flexible, but the simplest thing is just creating a single Rakefile
file in your directory, and putting all the code there.
Let's give it a go:
task :default => :hello
desc "Say helo"
task :hello do
puts "Hello, World!"
end
task
block defines a task. desc
before it provides a simple description. Tasks can have dependencies which you can specify with =>
.
Given this Rakefile we can do things like:
$ rake hello
Hello, World!
Not passing any arguments is same as rake default
:
$ rake
Hello, World!
rake -T
lists all available tasks. Tasks without descriptions are treated as internal and not displayed by default:
$ rake -T
rake hello # Say helo
If you want to see those internal tasks you can also pass -A
:
$ rake -T -A
rake default #
rake hello # Say helo
And for sake of debugging, if you pass --trace
, Rake
will tell you exactly what it's doing:
$ rake --trace
** Invoke default (first_time)
** Invoke hello (first_time)
** Execute hello
Hello, World!
** Execute default
Where "invoke" means task was requested (which then invokes dependencies), and "execute" that it was actually executed (after its dependencies). If there's complicated dependency graph, Rake keeps tracks of which tasks it already did, so it won't do something twice for no reason.
File tasks
One common category of tasks are file tasks. You can define them with file
and list their file dependencies. If you run rake hello.html
, it will only actually do anything if hello.md
changed. It does it by comparing last modified date.
task :default => "hello.html"
file "hello.html" => "hello.md" do
sh "pandoc hello.md -o hello.html"
end
Rules
There's also a shortcut for defining that every *.foo
file can be built from *.bar
files.
Tasks are defined as blocks, and their first argument (usually ignored) is the task object itself. t.name
is the task or file we're doing, t.source
is the source, and so on.
task :default => "hello.html"
rule ".html" => ".md" do |t|
sh "pandoc", t.source, "-o", t.name
end
As you can see, we can pass array of arguments to sh
command. This way Rake has zero issues with escaping file names with special characters. So many Unix shell scripts and Make files just crash if file names contain something as exotic as spaces. It's one of so many advantages of using a real programming language and not shell.
Fibonacci
Rake has a lot more features, but what I covered should be enough to get you started. So let's use it for something it was absolutely not designed to do, starting with the Fibonacci sequence.
require "pathname"
(1..2).each do |i|
file "#{i}.txt" do |t|
Pathname(t.name).write "1"
end
end
(3..100).each do |i|
file "#{i}.txt" => ["#{i-1}.txt", "#{i-2}.txt"] do |t|
Pathname(t.name).write t.sources.map{|n| Pathname(n).read.to_i}.sum
end
end
desc "Cleanup Fibonacci files"
task "clean" do
sh "trash *.txt"
end
For 1.txt
and 2.txt
the rule is very simple - we create them by writing 1
to the file.
For every other file like "75.txt", it depends on previous two files "73.txt" and "74.txt". The task reads the source files, adds them, and writes the result to target file.
There's also a cleanup task.
$ rake 100.txt
$ cat 100.txt
354224848179261915075
But let's see exactly what's happening when we ask Rake to create some file:
$ rake --trace 10.txt
** Invoke 10.txt (first_time)
** Invoke 9.txt (first_time)
** Invoke 8.txt (first_time)
** Invoke 7.txt (first_time)
** Invoke 6.txt (first_time)
** Invoke 5.txt (first_time)
** Invoke 4.txt (first_time)
** Invoke 3.txt (first_time)
** Invoke 2.txt (first_time)
** Execute 2.txt
** Invoke 1.txt (first_time)
** Execute 1.txt
** Execute 3.txt
** Invoke 2.txt (not_needed)
** Execute 4.txt
** Invoke 3.txt (not_needed)
** Execute 5.txt
** Invoke 4.txt (not_needed)
** Execute 6.txt
** Invoke 5.txt (not_needed)
** Execute 7.txt
** Invoke 6.txt (not_needed)
** Execute 8.txt
** Invoke 7.txt (not_needed)
** Execute 9.txt
** Invoke 8.txt (not_needed)
** Execute 10.txt
And when we run it the second time, when all the files already exist:
$ rake --trace 10.txt
** Invoke 10.txt (first_time, not_needed)
** Invoke 9.txt (first_time, not_needed)
** Invoke 8.txt (first_time, not_needed)
** Invoke 7.txt (first_time, not_needed)
** Invoke 6.txt (first_time, not_needed)
** Invoke 5.txt (first_time, not_needed)
** Invoke 4.txt (first_time, not_needed)
** Invoke 3.txt (first_time, not_needed)
** Invoke 2.txt (first_time, not_needed)
** Invoke 1.txt (first_time, not_needed)
** Invoke 2.txt (not_needed)
** Invoke 3.txt (not_needed)
** Invoke 4.txt (not_needed)
** Invoke 5.txt (not_needed)
** Invoke 6.txt (not_needed)
** Invoke 7.txt (not_needed)
** Invoke 8.txt (not_needed)
FizzBuzz
It doesn't make much sense to do FizzBuzz with a dependency manager, but let's do it anyway:
desc "FizzBuzz"
task "fizzbuzz", [:n] do |t, args|
n = args.n.to_i
puts ["Fizz"[(n % 3)*4..], "Buzz"[(n % 5)*4..], " #{n}"].join.split.first
end
Rake tasks can specify optional arguments. You can call them with:
$ rake 'fizzbuzz[1]'
1
$ rake "fizzbuzz[5]"
Buzz
$ rake "fizzbuzz[9]"
Fizz
$ rake "fizzbuzz[15]"
FizzBuzz
Depending on shell, you might or might not need to quote the square brackets. In zsh you do.
The FizzBuzz line is just mildly obfuscated Ruby, I'll leave it to you as a fun little puzzle.
Should you use Rake?
Absolutely! It's by far the best general purpose dependency manager, and Ruby is a delight to work with. I 100% endorse it.
I only showed some very basic examples, but Rake has convenient methods for all common use cases, and Ruby has far better support for Unix tasks than any other language by a huge margin.
Code
All code examples for the series will be in this repository.