Saturday, January 14, 2012

Rake Fundamentals: file tasks

The file task

The “file” task keyword is a call to a method called, you guessed it, “file()”.  Like the directory task, it’s purpose is to create the thing that it names.  But making a file is a bit more involved than making a directory – you have to know what goes into the file when you make it.  What’s the name and extension of the file?  What’s the source for the data in the file?  Should there be any kind of transformation, reformatting, or translation of the source data to the data to be saved in the file?  What directory should you save the file in?  You can provide all that info to the file task when you define it.  Here’s some pseudo-code for a file task:

desc "copy source file to target file"
file target_file => source_file do
cp source_file, target_file
end 

This isn’t a real file task, don’t try to run it.  But what’s going on here?  The name of this task is “target_file”, and it has a prerequisite called “source_file”.  And if the file task decides that it’s supposed to execute the do…end block, rake uses the “cp” command to copy the file named “source_file” to a file named “target_file”.  How does the file task decide that the do…end block needs to be executed?  Two things factor into that decision: 1) if target_file doesn’t exist, then it obviously needs to be created, so the do…end block gets executed.  2) If the source_file is newer than the target_file, that means that the source_file has changed since the last time that the target_file was created, and the do…end block gets executed again so that the target_file will get refreshed with the changes in source_file.

Ok, let’s pretend.  Let’s go back to our little rakefile we used to create directories.  And let’s pretend that we have a source/bin directory that contains some binary files that are the result of compilation – we’ll call them “file1.bin”, “file2.bin”, and “file3.bin”.  And furthermore, let’s pretend that we want to package those files up for distribution.  We can write some file tasks to make that happen:

require 'rake'

BIN_DIR = "source/bin"
PACKAGE_DIR = "package"

desc "create a #{BIN_DIR} directory"
directory BIN_DIR

desc "create a #{PACKAGE_DIR} directory"
directory PACKAGE_DIR


desc "copy file1.bin to package"
file "package/file1.bin" => [PACKAGE_DIR, "source/bin/file1.bin"] do |t|
cp t.prerequisites[1], t.name, :verbose => true
end

desc "copy file2.bin to package"
file "package/file2.bin" => [PACKAGE_DIR, File.join(BIN_DIR, "file2.bin")] do |t|
cp t.prerequisites[1], t.name, :verbose => true
end

desc "copy file3.bin to package"
file "package/file3.bin" => [PACKAGE_DIR, File.join(BIN_DIR, "file3.bin")] do |t|
cp t.prerequisites[1], t.name, :verbose => true
end

And here’s our task list now:

image

In our file “package/file1.bin” task, there are two prerequisites: PACKAGE_DIR and “source/bin/file1.bin”.  If we execute “rake package/file1.bin”, the first thing that will happen is the package directory will get created since it doesn’t already exist.  And since package/file1.bin doesn’t exist either, our do…end block will get executed.  That do…end block looks a bit different than anything we’ve seen so far, but it’s really not that complicated.  Just like a lambda in c#, you can pass parameters to a do…end block in Ruby – in this case |t| represents the file task, and we’re passing it into our do…end block so that we can have access to some info about the task, particularly the prerequisites list and the name of the task.  “cp t.prerequisites[1], t.name, :verbose => true” is very similar to the “cp source_file, target_file” line from the pseudo-code file task above.  In this case, the source_file is “t.prerequisites[1]” – in other words, the source_file is “source/bin/file1.bin”, which is the value in element 1 of the file task’s prerequisites array.  The target_file is “t.name” – in other words, the target_file is “package/file1.bin”, the name of the file task.  And the “:verbose => true” thing just means “tell me what you’re doing when you copy this file.”  If we run this task, here’s what we get:

image

Before we run the task, there’s no package directory.  After we run the task, there’s a package directory and it contains file1.bin.  Cool.

The file “package/file2.bin” task looks a little bit different. Instead of a source_file prerequisite called “source/bin/file2.bin”, we’ve got this:

File.join(BIN_DIR, “file2.bin”)

File.join() is a convenient Ruby method for formatting string input into properly structured path+filenames.  “source/bin” and “file2.bin” are appended together and separated by a “/” character, and the result is “source/bin/file2.bin”.

I see you rolling your eyes again: whooptie-do, we can make files from the command line.  Yep, I agree, the way we’ve set up our rakefile so far doesn’t really give us a compelling reason to use file and directory tasks.  Calling each file task from the command line seems really cumbersome.  What if we had a “publish” task that published all three of our *.bin files to the /package directory in one fell swoop?  Let’s add this to the end of our rakefile:

desc "copy file1.bin to package"
task :publish => "package/file1.bin"

desc "copy file2.bin to package"
task :publish => "package/file2.bin"

desc "copy file3.bin to package"
task :publish => "package/file3.bin"

It looks like we’re adding three “publish” tasks, but we’re really only adding one:

image

Ruby allows you to do some pretty fancy dynamic stuff, and rake takes advantage of that by allowing you to “re-open” a task that has already been defined and add some more stuff to it.  In our case, we’re re-opening the “publish” task and adding a new prerequisite for each of our *.bin files.  The result is a single publish task that copies all three *.bin files to the /package directory:

image

Ok, now we’re getting somewhere.  Now if only one of our source/bin/*.bin files changes, only the changed file will get copied to the /package folder.  Open up source/bin/file2.bin and put some random text in it, and then run “rake publish” again:

image

We’re making progress, but our rakefile is still way too verbose.  We need to find a way to reduce the amount of code it takes to copy these files out to /package.  And we should also be able to copy an arbitrary list of files, not just a discreet list we have to keep updating.

Next up: FileLists

No comments: