This is the second in a series on Rake, see previous post for introduction on the Rakefile format and about global tasks with Rake.
In this post we’re going to look at another capability of Rake: file tasks. We’ll cover how create them, how they work, and then create a useful example. But, before we get into file tasks, we need to have a better understanding of another aspect of the Rakefile format: prerequisites.
Task Prerequisites
Any Rake task can optionally have one, or more, prerequisite tasks – also referred to as dependencies. As with any other Rake task, a prerequisite task is only executed if it is needed, and if it is executed it is only ever done so once.
Let’s start by declaring a couple tasks called one
and two
in our
Rakefile:
task 'one' do
puts 'one'
end
task 'two' do
puts 'two'
end
We can run the tasks in a shell, as we’ve seen before:
$ rake one
one
$ rake two
two
Now let’s declare one
as a prerequisite for two
:
task 'one' do
puts 'one'
end
task 'two' => ['one'] do
puts 'two'
end
As you can see, we haven’t changed how one
was defined at all. But,
for two
, we added => ['one']
after the task name: this is how you
declare a task’s prerequisites. At first this format may seem foreign,
but keep in mind, this is just Ruby code; let’s see how our Rakefile
would look like if we added in all of Ruby’s optional syntax:
task('one') do
puts 'one'
end
task({'two' => ['one']}) do
puts 'two'
end
As you can see, in both cases, we’re just passing 1 argument and a block
to the task
method. For one
, we pass in just the task name, 'one'
;
for two
, we’re passing in a hash, {'two' => ['one']}
, the hash key
is the task name 'two'
and the hash value is an array of prerequisites
['one']
.
Aside: If your task only has one prerequisite, the hash value doesn’t need to be an array:
task 'two' => 'one' do
puts 'two'
end
Let’s see what happens when we run each of those tasks now:
$ rake one
one
$ rake two
one
two
As you can see above, when we ran two
, we get the output from both the
one
and two
tasks. Now that we have a foundation for prerequisite
tasks to build on, let’s look at file tasks.
File Tasks
Thus far, all of the tasks we’ve created have used Rake’s task
method
to declare the task, but for file tasks Rake has a special method:
file
. File tasks in Rake are very similar to normal tasks: they have a
name, they can have zero or more actions, they can have prerequisites,
and if Rake determines the task needs to be run it will only be run
once. Now, the twist is that those things get modified to be file
related: the name of the task is the same as the file’s name, Rake
determines that a file task needs to be run if the file doesn’t exist or
if any of the prerequisite file tasks are newer.
That’s a bit to wrap your head around, so let’s look at some examples:
file 'foo.txt' do
touch 'foo.txt'
end
Even though file tasks are meant for dealing with files, you are still responsible for creating the file in the task’s action if the file doesn’t exist.
Aside: Rake includes a modified version of the
FileUtils
module so that you have access to its methods
in your task actions, which is where that touch
method above is from.
Aside: FileUtils.touch
works like the Unix
touch program: updates a file’s timestamps and creates
nonexistent files.
And, now when we run that task:
$ rake foo.txt
touch foo.txt # output from our file task when it's run
$ ls # showing file was created
foo.txt
Earlier, I mentioned that Rake will not run a file task if the file exists; so let’s see what happens when I delete the file and then run the task twice:
$ rm foo.txt # deleting file
$ rake foo.txt # running file task
touch foo.txt # output from our task
$ rake foo.txt # running file task again, but no output
$ ls # showing file was created
foo.txt
As we can see, the first time we ran the foo.txt
task we see the
touch foo.txt
output from the file being created, but the second time
we ran the task we get no such output. But, things behave a bit
different if we add a prerequisite to our file task:
file 'foo.txt' => 'bar.txt' do
touch 'foo.txt'
end
Aside: If the prerequisite for a file task is another file, you do not need to create an explicit file task for the prerequisite, just using the name of the file is enough.
$ ls # showing foo.txt does not exist
bar.txt
$ rake foo.txt # running file task
touch foo.txt # output from file task
$ rake foo.txt # running file task again, but no output
$ ls # showing file was created
bar.txt foo.txt
So far, things don’t seem much different: our file task creates the file if it doesn’t exist, and if we run the task again nothing happens.
$ touch bar.txt # update timestamp of prerequisite file
$ rake foo.txt # running file task again
touch foo.txt # output! the file was updated!
Because the timestamp for the bar.txt
file was newer than that of the
foo.txt
file, Rake executes the actions for the foo.txt
task.
Useful Example
With this series, I’m trying to show you a feature of Rake, then show a useful example of using that feature, hoping that it’ll spark an idea for how you can use Rake in your normal development process; this post is no exception.
In our Rails applications, we typically have a number of configuration files that are critical for the application to run correctly. But, because these files contain either sensitive information or settings specific to where it’s being run, we do not put these files in source control; instead we usually add an “example” file with dummy data, so those who begin working on our application later know what needs to be set. Well, we can use Rake to simplify the creation of our configuration files from these “example” files.
So, let’s say we want to create the config/database.yml
file from the
config/database.yml.example
file:
file 'config/database.yml' => 'config/database.yml.example' do
cp 'config/database.yml.example', 'config/database.yml'
end
Aside: Here we are using FileUtils
’s cp
(short
for: copy) method.
$ ls config/ # showing database.yml does not exist
database.yml.example
$ rake config/database.yml # running file task
cp database.yml.example database.yml # output from file task
$ ls config/ # showing file was created
database.yml database.yml.example
Okay, so our task is working properly, and copying the example file as
expected. I’m not a huge fan of our task definition as it stands:
there’s too much repetition, and we can clean that up. When creating a
task, the block that you pass to the task
method can also take an
argument, which will be the task itself. We can use this task argument
to DRY up our task definition:
file 'config/database.yml' => 'config/database.yml.example' do |task|
cp task.prerequisites.first, task.name
end
That looks much better, and it still behaves the same. With this task in
place, anyone who joins the project can run the task and will then have a
config/database.yml
to use. If they happen to run it again, nothing
will happen; until someone updates the config/database.yml.example
file, at which point you can then run this task again and get the latest
changes.
That means you can think of these “example” files as templates
for the actual files we need. Granted, it would be nice if the task
didn’t just overwrite our config/database.yml
with the contents of the
example file and instead allowed it to merge the two files together; in
future posts in this series we’ll be looking at expanding this task to
do just that!