Rake Rocks!

I was initially attracted to Ruby because of Martin Fowler’s article on Rake. Fresh from a battle with a large Makefile, and having had sufficient experience with ANT, I was ready for something better. Rake is most definitely it. Here’s why.

Originally published 2006

Rake has a number of advantages over other build languages like Make and Ant:

  • No whitespace syntax
    There are three kinds of whitespace in a Makefile: spaces, tabs, and newlines. And they’re all significant. Lines that define a task have to be indented with a tab, or the build fails. Lines in an ifdef have to be indented with spaces, or the build fails. Continued lines have to end with a backslash (\), and the line terminator has to immediately follow the terminator. If there is a space or tab there, the build fails.

    Now, computer syntax is hard enough. But when your syntax depends on different flavors whitespace? Come on! It’s not like you can just look at the file to figure out what’s wrong. Rake gets rid of that nonsense. Whitespace is whitespace. It’s there for readability.

  • No single-line shell execution
    In Makefiles, every line of a task runs in it’s own independent shell. So if you want to cd to a subdirectory, to have to join that command to the subsequent one with a backslash-continuation, and add a semi-colon to separate the commands. Forget the semi-colon, and the build fails. Forget the continuation line and it “succeeds”, but it does not produce the results you intend. Forget that noise, as well. Rake tasks are, in reality, method definitions. So all of the commands you specify run together, the way you intended.
     
  • No XML syntax
    ANT avoids the single-line shell problem and the whitespace problem, but at the expense of highly verbose, persnickety XML syntax. It really seemed like the right way to go at the time, but in retrospect even James Davidson came to think of it as a mistake. (See these comments, referenced in Ben Galbraith’s excellent post, How I Learned to Love Domain-Specific Languages. Also, note that Jim’s articles show that he has been doing a fair amount of work with Rails. :_)
     
  • Plain tasks and file tasks
    Like Make, Rake knows that if file y depends on file x, the task that creates y needs to be run if y is older than x. You specify that information with the command file “y” => “x”. But you can also define tasks that don’t necessarily depend on a particular file, and chain them to other tasks:  task :build_z => [:build_x, :build_y].
     
  • You can define your own dependency rules
    Make understands that you get a .o file by compiling a .c file. But what does it know about Java files? Ant added that knowledge for Java compilation, but neither of them knows about Javadoc, or about man page conversions. With Rake, you can define a rule to do that (rule …). Then, anytime a dependent file needs to be created, and no task has been defined for it, Rake uses that rule to synthesize the task you need.
     
  • You can write subroutines, and even call tasks from other tasks
    Since Rake is a domain-specific extension of Ruby, all of Ruby is at your disposal when you write a Rakefile. Need a function subroutine? No problem. Need a loop? No problem. These things are a very big deal in Make and Ant. But in Rake, they’re no problem at all.
     
  • Easy access to standard shell operations
    There three ways to access shell operations, in fact:

    • Many operations like cp, mkdir_p are built in, so they’re easy to use, and they run on any operating system.
    • sh lets you execute a command in a subshell, embedding information from the build:
         sh “cd #{DIR}; doSomething
      Here, the operating system shell’s cd command is used to change the working directory. But since it is operating in a subshell, it doesn’t affect the current location of the script. And note the #{…}notation. That’s one of Ruby’s nifty features that lets you insert the value of a variable into a string. So it’s easy to construct the command you want to execute.
    • `backtick notation` gets back the results of executing a shell command
       
  • FileLists make it easy to work with sets of files
    As with procedural programming, you look at the list of what you have to create a list of what you need to produce. But in a procedural program, you’re always building in everything, unless you write the code to do date comparisons. With dependencies, on the other hand, you only create what doesn’t exist or has gone out of date. With Rake, you get the best of both worlds. You process
     
  • Tasks are cumulative
    You can define a task in one location (task :foo). You can put the processing instructions somewhere else (task :foo do |t| … end). You can specify a dependency somewhere else — and any dependency you define adds to the dependency list for that task (task :foo => […]). This capability is part of the secret to Rake’s power. For example, you can keep adding to the dependency list of a given task by processing a FileList. There is a real force multiplier advantage there, because in one FileList you can add to dependencies to multiple tasks, and you can easily build dependencies for the same task from multiple sets of files.

Putting it all Together

The combination of FileLists and cumulative task definitions really came to my rescue when I was setting up a setting up a translation system for man pages. This section outlines the Rakefile that resulted.

The source files for the man pages came from multiple directories, one for pages that were unique to Solaris, one for pages that were unique to Linux, and one for pages that were shared between the two systems. For shared pages, there was a two-step translation in which Solaris pages were generated, after which they were converted to the Linux format — so the Linux operations had to be performed after the Solaris operations. The trick was to get all this done in a build script that would minimize the amount of work that was performed. The solution looked something like this:

task :default => [:solaris, :linux]

task :linux => [:solaris]

task :solaris

FileList["share/*"].each do
  # "compute solaris path"
	 file solarispath => [srcfile] do
     # "generate the file"
  end
  task :solaris => solarispath
	
  # "compute linux path"
  file linuxpath => [solarispath] do
     # "generate the file"
  end
  task :linux => linuxpath
end

...

The task dependencies chain the linux and solaris tasks, ensuring that the solaris task runs first. (That may not actually have been necessary, but it seemed like a good idea.) Next, the FileList code process the files in the share/ directory. It creates a file task for each file in the directory, so that the operation is only performed if it is needed, and adds that file as a dependency to the solaris task. It then does the same thing for the linux task.

In this FileList, a single directory was examined, and file dependencies were added to two tasks. File lists were also processed for the Solaris-only and Linux-only directories, adding additional dependencies to the appropriate task. In short, one file list produced dependencies for multiple tasks (1:many), and multiple file lists were processed to create dependencies for a single task (many:1). The result was a many:many system of dependencies that buiilds all of the pages that have changed, and only the pages that have changed.

It took a while to figure out, but once I did, it was sweet. That experiences capture the flavor of my experience with Ruby so far: It’s taking me a while to figure out how to do things, but once I do, the results are elegant and pleasing.

Conclusion

Rake is a powerful, extremely expressive build language. To learn more:

Copyright © 2006-2017, TreeLight PenWorks

Add a Comment

1 Comment on "Rake Rocks!"

...

Sort by:   newest | oldest | most voted
trackback
[…] Rake The Ruby-based build tool. It looks like Make, but avoids Make’s whitespace weirdness, where spaces and tabs act differently, and where indenting, or failing to so, changes the way the instructions are interpreted. Because it avoids XML tags, it easier to write and read than the ANT build tool, but at the same time, it lets you write full Ruby-language procedures whenever you need to, and because you can define your own dependencies, it is superior to either of its main competitors. (This project takes it for granted that Rake will be the build tool of choice. It… Read more »
wpDiscuz

Categories