100 Languages Speedrun: Episode 17: Rake
Tags: ruby, rake, unix
Published: 2021-12-07 17:35:22

Once upon a time, people were coding in C, and building a full program was a convoluted process taking many steps. [Make](https://en.wikipedia.org/wiki/Make_(software)) 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:

```ruby
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.

```ruby
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.

```ruby
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.

```ruby
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:

```ruby
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](https://github.com/taw/100-languages-speedrun).

[Code for the Rake episode is available here](https://github.com/taw/100-languages-speedrun/tree/master/episode-17-rake).