It’s odd how ideas come to you. I was working on a script unrelated to
Rake, when a different way of using Rake occurred to me.
Start / Stop Script
Today a coworker passed on a shell script for staring and stopping a server on
a local box. The script was pretty typical. You would type “server
start” and “server stop” to bring the aforementioned server up
and down. It looked something like this:
There were a few more options, but you get the drift. Anyways, I wanted to to customize the script a bit and realized that my shell scripting abilities were a bit rusty. So I translated the whole thing to Ruby …
The Ruby Version
The translation was pretty straight forward. The guts of the script looked like this:
ARGV.each do |arg|
case arg
when 'start'
Dir.chdir(WLS) do
system "./startWebLogic.sh >out.log &"
end
when 'debug'
Dir.chdir(WLS) do
system "./startWebLogicDEBUG.sh >out.log &"
end
# [... code elided ...]
and so on.
After about the second or third “when” clause in the “case” statement, I
realized that I was essentially writing tasks. And we have a perfectly good
Ruby tool for managing task based software: Rake!
So why wasn’t I writing this as a Rake script? One reason is that Rake depends
on the presence of a Rakefile in the current (or parent) directories. I needed
to be able to run any of my server commands from anywhere in the file system,
not just from the directory containing the Rakefile.
Fair enough. But there’s nothing preventing me from writing a ruby script that
includes the Rake library. We saw something similar in the FindInCode script
earlier. The only additional thing we need to do is include the task
definitions directly in the script, and then explicit invoke the tasks we
need.
The Initial Rake Version
So I abandoned my first cut at a Ruby version and went with this:
#!/usr/bin/env ruby
require 'rake'
SERVER_DIR = /path/to/server/directory
task :start do
Dir.chdir(SERVER_DIR) do
sh "./startServer.sh >out.log &"
end
end
task :stop do
Dir.chdir(SERVER_DIR) do
sh "./stopServer.sh >out.log &"
end
end
# [... more task defintions go here ...]
ARGV.each do |arg| Rake::Task[arg].invoke end
Now I have a version that can handle all the task dependency stuff that
Rakefiles do, but can be run from anywhere in the file system. Assuming I
named the script “server”, I can say:
server start
and the right code gets run. I can specify dependencies between the tasks in
the script (e.g. “task :bounce => [:stop, :start]“), and do all the
other great stuff that building on Rake allows.
The Improved Rake Version
There are a couple of downsides to the above script. Even though I can invoke
the server script as if it were a rake-like command, it doesn’t do everything
that rake does. For example, there is no way to get a list of all the defined
tasks (.e.g rake -T). In fact, no rake command line options are
support in the above code. Neither are the environment variable parameters
(e.g. TEST=test_file_name.rb). Furthermore, the error messages are not trapped
and handled the way that Rake does it.
Now, all of the above could be manually added to the server script, but a
better way is to slightly refactor Rake to better support customer rake
applications. The init and top_level public methods were added to the Rake
Application class, so now we can write the above script as:
#!/usr/bin/env ruby
gem 'rake', '>= 0.7.3'
require 'rake'
Rake.application.init('server')
SERVER_DIR = /path/to/server/directory
task :start do
Dir.chdir(SERVER_DIR) do
sh "./startServer.sh >out.log &"
end
end
task :stop do
Dir.chdir(SERVER_DIR) do
sh "./stopServer.sh >out.log &"
end
end
# [... more task defintions go here ...]
Rake.application.top_level
All we did was add a call to Rake.application.init to initialize the command
line parameter information in the Rake application. You can specify an
optional application name to init to allow the Rake software to correctly
report the application name when handling a -T command line option.
The second thing we did was change the explicit loop through all the
parameters at the end of the script into a single call to top_level. The
top_level method not only handles the invocation of the top level tasks, but
also is smart enough about the Rake command line options to properly handle
them.
One final note about the “gem” command line near the top. The init and
top_level methods were introduced in version 0.7.3 of Rake. By using the gem
command we can ensure that are using a compatible version of the Rake
software.
Summary
When should you use a normal Rakefile, and when should you write a custom Rake application? A Rakefile works best for per-project type of commands, such as those used to build and test your individual projects. You are (almost) always somewhere in the project directory tree when invoking Rake, and so it just does the right thing.
Custom Rake applications work well when the command are not per-project, and you need to run them anywhere in the file system. A custom application gives you the freedom to run the command anywhere and not need a Rakefile first.
Here’s another quick example where I found a custom Rake application to be
useful. Lately I have been doing a lot of small Ruby scripts. I have setup
TextMate to autorun the tests via rake, but that means before I can test any
of my mini-ruby program, I need to take the time to write a Rakefile to run my
tests. Normally I would use rake to automate such a repetitive task, but rake
isn’t effective until I have a Rakefile, so we’re in a kind of Catch-22
situation. But with a custom Rake application, I can say something like “proj
rakefile” ... and have proj do all the heavy lifting for me without writing an
explicit Rakefile first.